diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt index 23c92f21..691df5b8 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt @@ -27,9 +27,21 @@ import org.domaframework.doma.intellij.extension.psi.isDataType import org.domaframework.doma.intellij.extension.psi.isDomain import org.domaframework.doma.intellij.extension.psi.isEntity import org.domaframework.doma.intellij.formatter.block.SqlBlock +import org.domaframework.doma.intellij.formatter.block.comma.SqlCommaBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateViewGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.with.SqlWithQuerySubGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlSubGroupBlock import kotlin.reflect.KClass object TypeUtil { + private val TOP_LEVEL_EXPECTED_TYPES = + listOf( + SqlSubGroupBlock::class, + SqlCommaBlock::class, + SqlWithQuerySubGroupBlock::class, + SqlCreateViewGroupBlock::class, + ) + /** * Unwraps the type parameter from Optional if present, otherwise returns the original type. */ @@ -118,6 +130,8 @@ object TypeUtil { return PsiTypeChecker.isBaseClassType(type) || DomaClassName.isOptionalWrapperType(type.canonicalText) } + fun isTopLevelExpectedType(childBlock: SqlBlock?): Boolean = isExpectedClassType(TOP_LEVEL_EXPECTED_TYPES, childBlock) + /** * Determines whether the specified class instance matches. */ diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlBlock.kt index 505e526b..b4a9facb 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlBlock.kt @@ -102,7 +102,9 @@ open class SqlBlock( protected fun isConditionLoopDirectiveRegisteredBeforeParent(): Boolean { val firstPrevBlock = (prevBlocks.lastOrNull() as? SqlElConditionLoopCommentBlock) parentBlock?.let { parent -> - return firstPrevBlock != null && + + return (childBlocks.firstOrNull() as? SqlElConditionLoopCommentBlock)?.isBeforeParentBlock() == true || + firstPrevBlock != null && firstPrevBlock.conditionEnd != null && firstPrevBlock.node.startOffset > parent.node.startOffset } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlFileBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlFileBlock.kt index ad4ac09e..2ba0ad40 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlFileBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlFileBlock.kt @@ -26,10 +26,6 @@ import com.intellij.formatting.Wrap import com.intellij.lang.ASTNode import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.formatter.common.AbstractBlock -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.psi.util.elementType -import com.intellij.psi.util.nextLeaf -import com.intellij.psi.util.nextLeafs import org.domaframework.doma.intellij.common.util.TypeUtil import org.domaframework.doma.intellij.formatter.block.comma.SqlCommaBlock import org.domaframework.doma.intellij.formatter.block.comment.SqlCommentBlock @@ -62,7 +58,6 @@ import org.domaframework.doma.intellij.formatter.block.other.SqlEscapeBlock import org.domaframework.doma.intellij.formatter.block.other.SqlOtherBlock import org.domaframework.doma.intellij.formatter.block.word.SqlAliasBlock import org.domaframework.doma.intellij.formatter.block.word.SqlArrayWordBlock -import org.domaframework.doma.intellij.formatter.block.word.SqlFunctionGroupBlock import org.domaframework.doma.intellij.formatter.block.word.SqlTableBlock import org.domaframework.doma.intellij.formatter.block.word.SqlWordBlock import org.domaframework.doma.intellij.formatter.builder.SqlBlockBuilder @@ -152,152 +147,130 @@ open class SqlFileBlock( child: ASTNode, prevBlock: SqlBlock?, ): SqlBlock { - val defaultFormatCtx = - SqlBlockFormattingContext( - wrap, - alignment, - spacingBuilder, - isEnableFormat(), - formatMode, - ) + val defaultFormatCtx = createDefaultFormattingContext() val lastGroup = blockBuilder.getLastGroupTopNodeIndexHistory() val lastGroupFilteredDirective = blockBuilder.getLastGroupFilterDirective() - return when (child.elementType) { - SqlTypes.KEYWORD -> { - return blockUtil.getKeywordBlock( - child, - blockBuilder.getLastGroupTopNodeIndexHistory(), - ) - } - - SqlTypes.DATATYPE -> { - SqlDataTypeBlock( - child, - defaultFormatCtx, - ) - } - - SqlTypes.LEFT_PAREN -> { - return blockUtil.getSubGroupBlock(lastGroup, child, blockBuilder.getGroupTopNodeIndexHistory()) - } - - SqlTypes.OTHER -> { - return if (lastGroup is SqlUpdateSetGroupBlock && - lastGroup.columnDefinitionGroupBlock != null - ) { - SqlUpdateColumnAssignmentSymbolBlock( - child, - defaultFormatCtx, - ) - } else { - val escapeStrings = listOf("\"", "`", "[", "]") - if (escapeStrings.contains(child.text)) { - if (child.text == "[" && prevBlock is SqlArrayWordBlock) { - SqlArrayListGroupBlock( - child, - defaultFormatCtx, - ) - } else { - SqlEscapeBlock( - child, - defaultFormatCtx, - ) - } - } else { - SqlOtherBlock( - child, - defaultFormatCtx, - ) - } - } - } - SqlTypes.RIGHT_PAREN -> return SqlRightPatternBlock( - child, - defaultFormatCtx, - ) + return when (child.elementType) { + SqlTypes.KEYWORD -> createKeywordBlock(child, lastGroup) + SqlTypes.DATATYPE -> SqlDataTypeBlock(child, defaultFormatCtx) + SqlTypes.LEFT_PAREN -> blockUtil.getSubGroupBlock(lastGroup, child, blockBuilder.getGroupTopNodeIndexHistory()) + SqlTypes.OTHER -> createOtherBlock(child, prevBlock, lastGroup, defaultFormatCtx) + SqlTypes.RIGHT_PAREN -> SqlRightPatternBlock(child, defaultFormatCtx) + SqlTypes.COMMA -> createCommaBlock(child, lastGroup, defaultFormatCtx) + SqlTypes.FUNCTION_NAME -> createFunctionNameBlock(child, lastGroup, defaultFormatCtx) + SqlTypes.WORD -> createWordBlock(child, lastGroup, defaultFormatCtx) + SqlTypes.BLOCK_COMMENT -> createBlockCommentBlock(child, lastGroup, lastGroupFilteredDirective, defaultFormatCtx) + SqlTypes.LINE_COMMENT -> SqlLineCommentBlock(child, defaultFormatCtx) + SqlTypes.PLUS, SqlTypes.MINUS, SqlTypes.ASTERISK, SqlTypes.SLASH -> SqlElSymbolBlock(child, defaultFormatCtx) + SqlTypes.LE, SqlTypes.LT, SqlTypes.EL_EQ, SqlTypes.EL_NE, SqlTypes.GE, SqlTypes.GT -> SqlElSymbolBlock(child, defaultFormatCtx) + SqlTypes.STRING, SqlTypes.NUMBER, SqlTypes.BOOLEAN -> SqlLiteralBlock(child, defaultFormatCtx) + else -> SqlUnknownBlock(child, defaultFormatCtx) + } + } - SqlTypes.COMMA -> { - return if (lastGroup is SqlWithQueryGroupBlock) { - SqlWithCommonTableGroupBlock(child, defaultFormatCtx) - } else { - blockUtil.getCommaGroupBlock(lastGroup, child) - } - } + private fun createDefaultFormattingContext(): SqlBlockFormattingContext = + SqlBlockFormattingContext( + wrap, + alignment, + spacingBuilder, + isEnableFormat(), + formatMode, + ) - SqlTypes.FUNCTION_NAME -> { - val notWhiteSpaceElement = - child.psi.nextLeafs - .takeWhile { it is PsiWhiteSpace } - .lastOrNull() - ?.nextLeaf(true) - if (notWhiteSpaceElement?.elementType == SqlTypes.LEFT_PAREN || - PsiTreeUtil.nextLeaf(child.psi)?.elementType == SqlTypes.LEFT_PAREN - ) { - return SqlFunctionGroupBlock(child, defaultFormatCtx) - } - return SqlKeywordBlock( - child, - IndentType.ATTACHED, - defaultFormatCtx, - ) - } + private fun createKeywordBlock( + child: ASTNode, + lastGroup: SqlBlock?, + ): SqlBlock { + if (blockUtil.hasEscapeBeforeWhiteSpace(blocks.lastOrNull() as? SqlBlock?, child)) { + return blockUtil.getWordBlock(lastGroup, child) + } + return blockUtil.getKeywordBlock( + child, + blockBuilder.getLastGroupTopNodeIndexHistory(), + ) + } - SqlTypes.WORD -> { - return if (lastGroup is SqlWithQueryGroupBlock) { - SqlWithCommonTableGroupBlock(child, defaultFormatCtx) - } else { - blockUtil.getWordBlock(lastGroup, child) - } - } + private fun createOtherBlock( + child: ASTNode, + prevBlock: SqlBlock?, + lastGroup: SqlBlock?, + defaultFormatCtx: SqlBlockFormattingContext, + ): SqlBlock { + if (lastGroup is SqlUpdateSetGroupBlock && lastGroup.columnDefinitionGroupBlock != null) { + return SqlUpdateColumnAssignmentSymbolBlock(child, defaultFormatCtx) + } - SqlTypes.BLOCK_COMMENT -> { - val tempBlock = - blockUtil.getBlockCommentBlock( - child, - createBlockDirectiveCommentSpacingBuilder(), - ) - if (tempBlock !is SqlElConditionLoopCommentBlock) { - if (lastGroup is SqlWithQueryGroupBlock || lastGroupFilteredDirective is SqlWithQueryGroupBlock) { - return SqlWithCommonTableGroupBlock(child, defaultFormatCtx) - } - } - return if (lastGroup is SqlWithCommonTableGroupBlock) { - SqlWithCommonTableGroupBlock(child, defaultFormatCtx) - } else { - tempBlock - } + val escapeStrings = listOf("\"", "`", "[", "]") + if (escapeStrings.contains(child.text)) { + return if (child.text == "[" && prevBlock is SqlArrayWordBlock) { + SqlArrayListGroupBlock(child, defaultFormatCtx) + } else { + SqlEscapeBlock(child, defaultFormatCtx) } + } + return SqlOtherBlock(child, defaultFormatCtx) + } - SqlTypes.LINE_COMMENT -> - return SqlLineCommentBlock( - child, - defaultFormatCtx, - ) - - SqlTypes.PLUS, SqlTypes.MINUS, SqlTypes.ASTERISK, SqlTypes.SLASH -> - return SqlElSymbolBlock( - child, - defaultFormatCtx, - ) + private fun createCommaBlock( + child: ASTNode, + lastGroup: SqlBlock?, + defaultFormatCtx: SqlBlockFormattingContext, + ): SqlBlock = + if (lastGroup is SqlWithQueryGroupBlock) { + SqlWithCommonTableGroupBlock(child, defaultFormatCtx) + } else { + blockUtil.getCommaGroupBlock(lastGroup, child) + } - SqlTypes.LE, SqlTypes.LT, SqlTypes.EL_EQ, SqlTypes.EL_NE, SqlTypes.GE, SqlTypes.GT -> - return SqlElSymbolBlock( - child, - defaultFormatCtx, - ) + private fun createFunctionNameBlock( + child: ASTNode, + lastGroup: SqlBlock?, + defaultFormatCtx: SqlBlockFormattingContext, + ): SqlBlock { + val block = blockUtil.getFunctionName(child, defaultFormatCtx) + if (block != null) { + return block + } + // If it is not followed by a left parenthesis, treat it as a word block + return if (lastGroup is SqlWithQueryGroupBlock) { + SqlWithCommonTableGroupBlock(child, defaultFormatCtx) + } else { + blockUtil.getWordBlock(lastGroup, child) + } + } - SqlTypes.STRING, SqlTypes.NUMBER, SqlTypes.BOOLEAN -> - return SqlLiteralBlock( - child, - defaultFormatCtx, - ) + private fun createWordBlock( + child: ASTNode, + lastGroup: SqlBlock?, + defaultFormatCtx: SqlBlockFormattingContext, + ): SqlBlock = + if (lastGroup is SqlWithQueryGroupBlock) { + SqlWithCommonTableGroupBlock(child, defaultFormatCtx) + } else { + blockUtil.getWordBlock(lastGroup, child) + } - else -> - SqlUnknownBlock( - child, - defaultFormatCtx, - ) + private fun createBlockCommentBlock( + child: ASTNode, + lastGroup: SqlBlock?, + lastGroupFilteredDirective: SqlBlock?, + defaultFormatCtx: SqlBlockFormattingContext, + ): SqlBlock { + val tempBlock = + blockUtil.getBlockCommentBlock( + child, + createBlockDirectiveCommentSpacingBuilder(), + ) + if (tempBlock !is SqlElConditionLoopCommentBlock) { + if (lastGroup is SqlWithQueryGroupBlock || lastGroupFilteredDirective is SqlWithQueryGroupBlock) { + return SqlWithCommonTableGroupBlock(child, defaultFormatCtx) + } + } + return if (lastGroup is SqlWithCommonTableGroupBlock) { + SqlWithCommonTableGroupBlock(child, defaultFormatCtx) + } else { + tempBlock } } @@ -554,7 +527,19 @@ open class SqlFileBlock( SqlCustomSpacingBuilder.nonSpacing } - else -> SqlCustomSpacingBuilder.normalSpacing + is SqlSubGroupBlock -> { + val includeSpaceRight = childBlock1.endPatternBlock?.isPreSpaceRight() + + if (includeSpaceRight == false) { + SqlCustomSpacingBuilder.nonSpacing + } else { + SqlCustomSpacingBuilder.normalSpacing + } + } + + else -> { + SqlCustomSpacingBuilder.normalSpacing + } } } @@ -576,6 +561,13 @@ open class SqlFileBlock( if (childBlock2.isEndEscape) { return SqlCustomSpacingBuilder.nonSpacing } + + // When a column definition is enclosed in escape characters, + // calculate the indentation to match the formatting rules of a CREATE query. + CreateClauseHandler + .getColumnDefinitionRawGroupSpacing(childBlock1, childBlock2) + ?.let { return it } + return SqlCustomSpacingBuilder().getSpacing(childBlock2) } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlRightPatternBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlRightPatternBlock.kt index d0677671..2b2a3b65 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlRightPatternBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/SqlRightPatternBlock.kt @@ -18,6 +18,7 @@ package org.domaframework.doma.intellij.formatter.block import com.intellij.lang.ASTNode import com.intellij.psi.formatter.common.AbstractBlock import org.domaframework.doma.intellij.common.util.TypeUtil.isExpectedClassType +import org.domaframework.doma.intellij.formatter.block.comment.SqlCommentBlock import org.domaframework.doma.intellij.formatter.block.conflict.SqlConflictExpressionSubGroupBlock import org.domaframework.doma.intellij.formatter.block.group.column.SqlColumnDefinitionRawGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.SqlKeywordGroupBlock @@ -26,6 +27,7 @@ import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlC import org.domaframework.doma.intellij.formatter.block.group.keyword.insert.SqlInsertColumnGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.insert.SqlInsertValueGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.second.SqlValuesGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.top.SqlTopQueryGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.update.SqlUpdateColumnGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.update.SqlUpdateSetGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.update.SqlUpdateValueGroupBlock @@ -60,6 +62,9 @@ open class SqlRightPatternBlock( } private var preSpaceRight = false + + fun isPreSpaceRight() = preSpaceRight + var lineBreakAndSpacingType: LineBreakAndSpacingType = LineBreakAndSpacingType.NONE companion object { @@ -77,7 +82,6 @@ open class SqlRightPatternBlock( SqlInsertColumnGroupBlock::class, SqlWithQuerySubGroupBlock::class, SqlConflictExpressionSubGroupBlock::class, - SqlConditionalExpressionGroupBlock::class, ) val NEW_LINE_EXPECTED_TYPES = @@ -103,11 +107,21 @@ open class SqlRightPatternBlock( */ private fun enableLastRight() { parentBlock?.let { parent -> + val isFirstChildQuery = + parent.childBlocks.firstOrNull { + it !is SqlCommentBlock + } is SqlTopQueryGroupBlock // Check if parent is in the notInsertSpaceClassList if (isExpectedClassType(NOT_INDENT_EXPECTED_TYPES, parent)) { preSpaceRight = false return } + + if (parent is SqlConditionalExpressionGroupBlock) { + preSpaceRight = isFirstChildQuery + return + } + if (isExpectedClassType( INDENT_EXPECTED_TYPES, parent, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElBlockCommentBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElBlockCommentBlock.kt index 7cf1ddde..5f560b36 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElBlockCommentBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElBlockCommentBlock.kt @@ -27,6 +27,7 @@ import org.domaframework.doma.intellij.formatter.block.SqlUnknownBlock import org.domaframework.doma.intellij.formatter.block.expr.SqlElFieldAccessBlock import org.domaframework.doma.intellij.formatter.block.expr.SqlElFunctionCallBlock import org.domaframework.doma.intellij.formatter.block.expr.SqlElStaticFieldAccessBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.SqlKeywordGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.second.SqlValuesGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.with.SqlWithQuerySubGroupBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlSubQueryGroupBlock @@ -168,6 +169,7 @@ open class SqlElBlockCommentBlock( } } is SqlValuesGroupBlock -> parent.indent.indentLen + is SqlKeywordGroupBlock -> parent.indent.groupIndentLen.plus(1) else -> parent.indent.groupIndentLen } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElConditionLoopCommentBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElConditionLoopCommentBlock.kt index 8306b514..17c206b7 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElConditionLoopCommentBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlElConditionLoopCommentBlock.kt @@ -28,6 +28,7 @@ import org.domaframework.doma.intellij.formatter.block.SqlRightPatternBlock import org.domaframework.doma.intellij.formatter.block.SqlUnknownBlock import org.domaframework.doma.intellij.formatter.block.group.column.SqlColumnRawGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.SqlKeywordGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.condition.SqlConditionalExpressionGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateKeywordGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.insert.SqlInsertQueryGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.with.SqlWithCommonTableGroupBlock @@ -217,9 +218,9 @@ class SqlElConditionLoopCommentBlock( return lastBlock.indent.indentLen } } - return parent.indent.groupIndentLen + - openConditionLoopDirectiveCount * DIRECTIVE_INDENT_STEP + - if (parent !is SqlWithQueryGroupBlock) 1 else 0 + val withQuerySpace = if (parent !is SqlWithQueryGroupBlock) 1 else 0 + return parent.indent.groupIndentLen.plus(withQuerySpace) + + openConditionLoopDirectiveCount * DIRECTIVE_INDENT_STEP } else -> return parent.indent.indentLen + openConditionLoopDirectiveCount * DIRECTIVE_INDENT_STEP } @@ -302,5 +303,6 @@ class SqlElConditionLoopCommentBlock( private fun shouldNotIndent(parent: SqlSubGroupBlock): Boolean = TypeUtil.isExpectedClassType(SqlRightPatternBlock.NOT_INDENT_EXPECTED_TYPES, parent) || - parent is SqlWithCommonTableGroupBlock + parent is SqlWithCommonTableGroupBlock || + parent is SqlConditionalExpressionGroupBlock } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/column/SqlColumnDefinitionRawGroupBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/column/SqlColumnDefinitionRawGroupBlock.kt index bb49e9f9..553b9064 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/column/SqlColumnDefinitionRawGroupBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/column/SqlColumnDefinitionRawGroupBlock.kt @@ -18,6 +18,7 @@ package org.domaframework.doma.intellij.formatter.block.group.column import com.intellij.lang.ASTNode import com.intellij.psi.formatter.common.AbstractBlock import org.domaframework.doma.intellij.formatter.block.SqlBlock +import org.domaframework.doma.intellij.formatter.block.other.SqlEscapeBlock import org.domaframework.doma.intellij.formatter.util.SqlBlockFormattingContext import org.domaframework.doma.intellij.psi.SqlTypes @@ -32,12 +33,25 @@ open class SqlColumnDefinitionRawGroupBlock( node, context, ) { - // TODO Customize indentation within an inline group - open val defaultOffset = 0 - val isFirstColumnRaw = node.elementType != SqlTypes.COMMA + companion object { + private const val DEFAULT_OFFSET = 0 + private const val ESCAPE_CHARS_LENGTH = 2 + private const val FIRST_COLUMN_INDENT = 1 + } + val isFirstColumnRaw = node.elementType != SqlTypes.COMMA open var columnBlock: SqlBlock? = if (isFirstColumnRaw) this else null + fun getColumnNameLength(): Int { + val columnNameLength = columnBlock?.getNodeText()?.length ?: 0 + val hasEscapeCharacters = columnBlock?.prevBlocks?.firstOrNull() is SqlEscapeBlock + return if (hasEscapeCharacters) { + columnNameLength + ESCAPE_CHARS_LENGTH + } else { + columnNameLength + } + } + override fun setParentGroupBlock(lastGroup: SqlBlock?) { super.setParentGroupBlock(lastGroup) indent.indentLen = createBlockIndentLen() @@ -47,9 +61,12 @@ open class SqlColumnDefinitionRawGroupBlock( override fun buildChildren(): MutableList = mutableListOf() /** - * Right-justify the longest column name in the column definition. + * Calculate indent length for column definition. + * First column has an indent of 1, others use default offset. */ - override fun createBlockIndentLen(): Int = if (isFirstColumnRaw) 1 else defaultOffset + private fun calculateIndentLength(): Int = if (isFirstColumnRaw) FIRST_COLUMN_INDENT else DEFAULT_OFFSET + + override fun createBlockIndentLen(): Int = calculateIndentLength() override fun isSaveSpace(lastGroup: SqlBlock?): Boolean = true } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/condition/SqlConditionalExpressionGroupBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/condition/SqlConditionalExpressionGroupBlock.kt index 99b1244a..d4fcb177 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/condition/SqlConditionalExpressionGroupBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/condition/SqlConditionalExpressionGroupBlock.kt @@ -49,7 +49,18 @@ class SqlConditionalExpressionGroupBlock( override fun createBlockIndentLen(): Int = parentBlock?.let { parent -> if (parent is SqlElConditionLoopCommentBlock) { - parent.indent.groupIndentLen + val groupIndentLen = parent.indent.groupIndentLen + val grand = parent.parentBlock + val directiveParentTextLen = + if (grand !is SqlElConditionLoopCommentBlock) { + grand + ?.getNodeText() + ?.length + ?.plus(1) ?: 0 + } else { + 0 + } + groupIndentLen + directiveParentTextLen } else { parent.indent.groupIndentLen.plus(1) } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/create/SqlCreateTableColumnDefinitionGroupBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/create/SqlCreateTableColumnDefinitionGroupBlock.kt index 0c787fc3..f26f4d9a 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/create/SqlCreateTableColumnDefinitionGroupBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/create/SqlCreateTableColumnDefinitionGroupBlock.kt @@ -41,20 +41,23 @@ class SqlCreateTableColumnDefinitionGroupBlock( node, context, ) { - // TODO Customize indentation - override val offset = 2 - private val groupOffset = 5 + companion object { + private const val COLUMN_INDENT_OFFSET = 2 + private const val GROUP_INDENT_OFFSET = 5 + } + + override val offset = COLUMN_INDENT_OFFSET val columnRawGroupBlocks = mutableListOf() fun getMaxColumnNameLength(): Int = columnRawGroupBlocks.maxOfOrNull { raw -> - raw.columnBlock?.getNodeText()?.length ?: 0 + raw.getColumnNameLength() } ?: 0 override fun setParentGroupBlock(lastGroup: SqlBlock?) { super.setParentGroupBlock(lastGroup) indent.indentLen = createBlockIndentLen() - indent.groupIndentLen = indent.indentLen.plus(groupOffset) + indent.groupIndentLen = indent.indentLen.plus(GROUP_INDENT_OFFSET) } override fun buildChildren(): MutableList = mutableListOf() diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/top/SqlTopQueryGroupBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/top/SqlTopQueryGroupBlock.kt index d388aa1b..1582bc90 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/top/SqlTopQueryGroupBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/group/keyword/top/SqlTopQueryGroupBlock.kt @@ -99,4 +99,14 @@ abstract class SqlTopQueryGroupBlock( return parent.indent.indentLen } + + override fun isSaveSpace(lastGroup: SqlBlock?): Boolean { + if (TypeUtil.isTopLevelExpectedType(lastGroup) && + lastGroup !is SqlWithQuerySubGroupBlock && + lastGroup !is SqlCreateViewGroupBlock + ) { + return false + } + return super.isSaveSpace(lastGroup) + } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/other/SqlEscapeBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/other/SqlEscapeBlock.kt index 4a3666e4..d9e99f8f 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/other/SqlEscapeBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/other/SqlEscapeBlock.kt @@ -17,7 +17,9 @@ package org.domaframework.doma.intellij.formatter.block.other import com.intellij.lang.ASTNode import org.domaframework.doma.intellij.formatter.block.SqlBlock +import org.domaframework.doma.intellij.formatter.block.comment.SqlElConditionLoopCommentBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlArrayListGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlSubQueryGroupBlock import org.domaframework.doma.intellij.formatter.util.SqlBlockFormattingContext class SqlEscapeBlock( @@ -34,12 +36,53 @@ class SqlEscapeBlock( } override fun createBlockIndentLen(): Int { - val hasEvenEscapeBlocks = parentBlock?.childBlocks?.count { it is SqlEscapeBlock }?.let { it % 2 == 0 } == true + val parentEscapeBlock = + if (parentBlock is SqlElConditionLoopCommentBlock) { + if (parentBlock?.parentBlock is SqlEscapeBlock)1 else 0 + } else { + 0 + } + val prevBlocks = parentBlock?.childBlocks?.count { it is SqlEscapeBlock }?.plus(parentEscapeBlock) ?: 0 + + val hasEvenEscapeBlocks = prevBlocks.let { it % 2 == 0 } == true isEndEscape = hasEvenEscapeBlocks || getNodeText() == "]" return if (isEndEscape) { 0 } else { - 1 + calculateIndentLen() } } + + private fun calculateIndentLen(): Int { + parentBlock?.let { parent -> + when (parent) { + is SqlSubQueryGroupBlock -> { + val parentIndentLen = parent.indent.groupIndentLen + val grand = parent.parentBlock + if (grand != null && grand.getNodeText().lowercase() == "create") { + val grandIndentLen = grand.indent.groupIndentLen + return grandIndentLen.plus(parentIndentLen).plus(1) + } + return parentIndentLen.plus(1) + } + + is SqlElConditionLoopCommentBlock -> { + return parent.indent.groupIndentLen + } + + else -> { + if (isSaveSpace(parentBlock))return parentBlock?.indent?.groupIndentLen ?: 1 + return 1 + } + } + } + return 1 + } + + override fun isSaveSpace(lastGroup: SqlBlock?): Boolean = + if (isEndEscape) { + false + } else { + super.isSaveSpace(lastGroup) + } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/word/SqlFunctionGroupBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/word/SqlFunctionGroupBlock.kt index cb549380..bbe8d724 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/word/SqlFunctionGroupBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/word/SqlFunctionGroupBlock.kt @@ -18,6 +18,7 @@ package org.domaframework.doma.intellij.formatter.block.word import com.intellij.lang.ASTNode import org.domaframework.doma.intellij.formatter.block.SqlBlock import org.domaframework.doma.intellij.formatter.block.comment.SqlDefaultCommentBlock +import org.domaframework.doma.intellij.formatter.block.comment.SqlElConditionLoopCommentBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlFunctionParamBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlSubGroupBlock import org.domaframework.doma.intellij.formatter.util.SqlBlockFormattingContext @@ -40,32 +41,48 @@ class SqlFunctionGroupBlock( override fun createBlockIndentLen(): Int = parentBlock?.indent?.groupIndentLen ?: 0 override fun createGroupIndentLen(): Int { - var baseIndent = 0 - parentBlock?.let { parent -> - val children = prevChildren.dropLast(1).filter { it !is SqlDefaultCommentBlock } - val prevBlocksLength = - children - .sumOf { prev -> - prev - .getChildrenTextLen() - .plus( - if (prev.node.elementType == SqlTypes.DOT || - prev.node.elementType == SqlTypes.RIGHT_PAREN - ) { - 0 - } else { - prev.getNodeText().length.plus(1) - }, - ) - }.plus(parent.indent.groupIndentLen) - baseIndent = - if (parent is SqlSubGroupBlock) { - // parent.indent.groupIndentLen - prevBlocksLength - } else { - prevBlocksLength.plus(1) - } - } + val baseIndent = + parentBlock?.let { parent -> + val children = prevChildren.dropLast(1).filter { it !is SqlDefaultCommentBlock } + val prevBlocksLength = calculatePrevBlocksLength(children, parent) + calculateBaseIndent(parent, prevBlocksLength) + } ?: 0 return baseIndent.plus(getNodeText().length) } + + private fun calculatePrevBlocksLength( + children: List, + parent: SqlBlock, + ): Int = + children + .sumOf { prev -> + prev + .getChildrenTextLen() + .plus( + if (prev.node.elementType == SqlTypes.DOT || + prev.node.elementType == SqlTypes.RIGHT_PAREN + ) { + 0 + } else { + prev.getNodeText().length.plus(1) + }, + ) + }.plus(parent.indent.groupIndentLen) + + private fun calculateBaseIndent( + parent: SqlBlock, + prevBlocksLength: Int, + ): Int = + when (parent) { + is SqlSubGroupBlock -> + prevBlocksLength + + is SqlElConditionLoopCommentBlock -> { + val directiveParent = parentBlock?.parentBlock + val directiveParentLen = directiveParent?.getNodeText()?.length?.plus(1) ?: 1 + prevBlocksLength.plus(directiveParentLen) + } + + else -> prevBlocksLength.plus(1) + } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlBlockRelationBuilder.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlBlockRelationBuilder.kt index 6ff1f11d..8d4591c0 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlBlockRelationBuilder.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlBlockRelationBuilder.kt @@ -68,12 +68,6 @@ class SqlBlockRelationBuilder( SqlExistsGroupBlock::class, ) - private val TOP_LEVEL_EXPECTED_TYPES = - listOf( - SqlSubGroupBlock::class, - SqlCreateViewGroupBlock::class, - ) - private val COLUMN_RAW_EXPECTED_TYPES = listOf( SqlColumnRawGroupBlock::class, @@ -179,7 +173,7 @@ class SqlBlockRelationBuilder( context: SetParentContext, ) { val parentBlock = - if (TypeUtil.isExpectedClassType(TOP_LEVEL_EXPECTED_TYPES, lastGroupBlock)) { + if (TypeUtil.isTopLevelExpectedType(lastGroupBlock)) { lastGroupBlock } else if (childBlock is SqlUpdateQueryGroupBlock) { UpdateClauseHandler.getParentGroupBlock(blockBuilder, childBlock) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/CreateClauseHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/CreateClauseHandler.kt index 94ac6d7b..49202a54 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/CreateClauseHandler.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/CreateClauseHandler.kt @@ -24,10 +24,15 @@ import org.domaframework.doma.intellij.formatter.block.group.column.SqlColumnBlo import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateKeywordGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateTableColumnDefinitionGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateTableColumnDefinitionRawGroupBlock +import org.domaframework.doma.intellij.formatter.block.other.SqlEscapeBlock import org.domaframework.doma.intellij.formatter.util.SqlBlockFormattingContext import org.domaframework.doma.intellij.psi.SqlTypes object CreateClauseHandler { + private const val COLUMN_DEFINITION_OFFSET = 5 + private const val COMMA_OFFSET = 2 + private const val SINGLE_SPACE = 1 + fun getCreateTableClauseSubGroup( lastGroup: SqlBlock, child: ASTNode, @@ -61,44 +66,70 @@ object CreateClauseHandler { fun getColumnDefinitionRawGroupSpacing( child1: Block?, child2: Block, - ): Spacing? { - // TODO Customize indentation - val offset = 5 - - // Top Column Definition Group Block - if (child1 is SqlWhitespaceBlock && child2 is SqlCreateTableColumnDefinitionRawGroupBlock) { - val columnDefinitionGroupBlock = - child2.parentBlock as? SqlCreateTableColumnDefinitionGroupBlock ?: return null - - if (child2.node.elementType == SqlTypes.COMMA) { - // If the child2 is a comma, it is not a column definition raw group block. - return Spacing.createSpacing(offset, offset, 0, false, 0, 0) + ): Spacing? = + when { + child1 is SqlWhitespaceBlock && child2 is SqlCreateTableColumnDefinitionRawGroupBlock -> { + calculateColumnDefinitionSpacing(child2) } - - val maxColumnName = columnDefinitionGroupBlock.getMaxColumnNameLength() - val diffColumnNameLen = - maxColumnName.minus(child2.columnBlock?.getNodeText()?.length ?: 0) - // If the longest column name is not in the top row, add two spaces for the "," to match the row with a comma. - var indentLen = offset.plus(diffColumnNameLen) - val maxColumnNameRaw = - columnDefinitionGroupBlock.columnRawGroupBlocks - .findLast { raw -> raw.columnBlock?.getNodeText()?.length == maxColumnName } - if (maxColumnNameRaw?.isFirstColumnRaw != true) { - indentLen = indentLen.plus(2) + child1 is SqlCreateTableColumnDefinitionRawGroupBlock && + (child2 is SqlColumnBlock || child2 is SqlEscapeBlock) -> { + calculateColumnSpacing(child1, child2) } + else -> null + } + + private fun calculateColumnDefinitionSpacing(columnDefBlock: SqlCreateTableColumnDefinitionRawGroupBlock): Spacing? { + val columnDefinitionGroupBlock = + columnDefBlock.parentBlock as? SqlCreateTableColumnDefinitionGroupBlock ?: return null - return Spacing.createSpacing(indentLen, indentLen, 0, false, 0, 0) + // If the child is a comma, it is not a column definition raw group block. + if (columnDefBlock.node.elementType == SqlTypes.COMMA) { + return createFixedSpacing(COLUMN_DEFINITION_OFFSET) } - if (child1 is SqlCreateTableColumnDefinitionRawGroupBlock && child2 is SqlColumnBlock) { - val columnDefinitionGroupBlock = - child1.parentBlock as? SqlCreateTableColumnDefinitionGroupBlock ?: return null + val maxColumnNameLength = columnDefinitionGroupBlock.getMaxColumnNameLength() + val currentColumnLength = columnDefBlock.getColumnNameLength() + val columnDifference = maxColumnNameLength - currentColumnLength + + var indentLen = COLUMN_DEFINITION_OFFSET + columnDifference - val maxColumnName = columnDefinitionGroupBlock.getMaxColumnNameLength() - val diffColumnNameLen = maxColumnName.minus(child2.getNodeText().length) - var indentLen = diffColumnNameLen.plus(1) - return Spacing.createSpacing(indentLen, indentLen, 0, false, 0, 0) + // If the longest column name is not in the top row, add comma offset + if (!isMaxColumnInFirstRow(columnDefinitionGroupBlock, maxColumnNameLength)) { + indentLen += COMMA_OFFSET } - return null + + return createFixedSpacing(indentLen) } + + private fun calculateColumnSpacing( + rawGroupBlock: SqlCreateTableColumnDefinitionRawGroupBlock, + columnBlock: Block, + ): Spacing? { + val columnDefinitionGroupBlock = + rawGroupBlock.parentBlock as? SqlCreateTableColumnDefinitionGroupBlock ?: return null + + val maxColumnNameLength = columnDefinitionGroupBlock.getMaxColumnNameLength() + val columnLength = + when (columnBlock) { + is SqlColumnBlock -> columnBlock.getNodeText().length + else -> rawGroupBlock.getColumnNameLength() + } + + val columnDifference = maxColumnNameLength - columnLength + val indentLen = columnDifference + SINGLE_SPACE + + return createFixedSpacing(indentLen) + } + + private fun isMaxColumnInFirstRow( + columnDefinitionGroupBlock: SqlCreateTableColumnDefinitionGroupBlock, + maxColumnNameLength: Int, + ): Boolean { + val maxColumnNameRaw = + columnDefinitionGroupBlock.columnRawGroupBlocks + .findLast { raw -> raw.columnBlock?.getNodeText()?.length == maxColumnNameLength } + return maxColumnNameRaw?.isFirstColumnRaw == true + } + + private fun createFixedSpacing(spaces: Int): Spacing = Spacing.createSpacing(spaces, spaces, 0, false, 0, 0) } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/NotQueryGroupHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/NotQueryGroupHandler.kt index 967adc9d..199fd1bc 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/NotQueryGroupHandler.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/handler/NotQueryGroupHandler.kt @@ -26,6 +26,7 @@ import org.domaframework.doma.intellij.formatter.block.group.keyword.condition.S import org.domaframework.doma.intellij.formatter.block.group.keyword.option.SqlInGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.second.SqlReturningGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.second.SqlValuesGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.second.SqlWhereGroupBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlFunctionParamBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlParallelListBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlValuesParamGroupBlock @@ -55,7 +56,9 @@ object NotQueryGroupHandler { else -> null } - private fun lastGroupParentConditionKeywordGroup(lastGroup: SqlBlock?): Boolean = lastGroup is SqlConditionKeywordGroupBlock + private fun lastGroupParentConditionKeywordGroup(lastGroup: SqlBlock?): Boolean = + lastGroup is SqlConditionKeywordGroupBlock || + lastGroup is SqlWhereGroupBlock /** * Creates a keyword group block for specific keywords. diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/InjectionSqlFormatter.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/InjectionSqlFormatter.kt index 4cc6ab3a..95711ca3 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/InjectionSqlFormatter.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/InjectionSqlFormatter.kt @@ -28,6 +28,7 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiFileFactory import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.PsiTreeUtil import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.formatter.visitor.FormattingTask @@ -42,13 +43,13 @@ class InjectionSqlFormatter( private val COMMENT_START_REGEX = Regex("^[ \t]*/[*][ \t]*\\*") } - private val baseIndent = createSpaceIndent(project) + private var baseIndent = createSpaceIndent(project) private fun createSpaceIndent(project: Project): String { val settings = CodeStyle.getSettings(project) val java = settings.getIndentOptions(JavaFileType.INSTANCE) val indentSize = java.INDENT_SIZE - val prefixLen = "@Sql(\"\"\"".length + val prefixLen = "@Sql(${TRIPLE_QUOTE}".length return StringUtil.SINGLE_SPACE.repeat(indentSize.plus(prefixLen)) } @@ -166,12 +167,44 @@ class InjectionSqlFormatter( // Create properly aligned literal text val literalText = createFormattedLiteralText(processedSql) + updateBaseIndent(task) + val normalizedText = normalizeIndentation(literalText) val newLiteral = elementFactory.createExpressionFromText(normalizedText, task.expression) return newLiteral.text } + /** + * When formatting styles other than Java code style change the indentation of a text block, + * reset the base indent using the number of spaces before the PsiLiteralExpression. + */ + private fun updateBaseIndent(task: FormattingTask) { + val input = PsiTreeUtil.prevLeaf(task.expression)?.text + val prevTexts = countCharsToLineBreak(task.expression).plus(TRIPLE_QUOTE.length) + if (input != null) { + val matches = StringUtil.SINGLE_SPACE.repeat(prevTexts) + baseIndent = matches + } + } + + private fun countCharsToLineBreak(expression: PsiLiteralExpression): Int { + var count = 0 + var leaf = PsiTreeUtil.prevLeaf(expression) + while (leaf != null) { + val text = leaf.text + for (i in text.length - 1 downTo 0) { + val c = text[i] + if (c == '\n' || c == '\r') { + return count + } + count++ + } + leaf = PsiTreeUtil.prevLeaf(leaf) + } + return count + } + /** * Creates a Java text block (triple-quoted string) from formatted SQL. */ diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPreProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPreProcessor.kt index 215a7366..73a97043 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPreProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPreProcessor.kt @@ -28,6 +28,7 @@ import com.intellij.psi.TokenType import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType +import com.intellij.psi.util.prevLeafs import org.domaframework.doma.intellij.common.util.InjectionSqlUtil.isInjectedSqlFile import org.domaframework.doma.intellij.common.util.PluginLoggerUtil import org.domaframework.doma.intellij.common.util.StringUtil.LINE_SEPARATE @@ -97,45 +98,51 @@ class SqlFormatPreProcessor : PreFormatProcessor { var index = keywordList.size var keywordIndex = replaceKeywordList.size - visitor.replaces.asReversed().forEach { - val textRangeStart = it.startOffset - val textRangeEnd = textRangeStart + it.text.length - if (it.elementType != TokenType.WHITE_SPACE) { + visitor.replaces.asReversed().forEach { current -> + val textRangeStart = current.startOffset + val textRangeEnd = textRangeStart + current.text.length + if (current.elementType != TokenType.WHITE_SPACE) { // Add a newline before any element that needs a newline+indent, without overlapping if there is already a newline index-- - var newKeyword = getUpperText(it) - when (it.elementType) { + var newKeyword = getUpperText(current) + when (current.elementType) { SqlTypes.KEYWORD -> { keywordIndex-- - newKeyword = getKeywordNewText(it) + // Escape-enclosed keywords are treated as regular words and are not converted to uppercase. + val escapes = current.prevLeafs.filter { it.elementType == SqlTypes.OTHER }.toList() + if (hasEscapeBeforeWhiteSpace(escapes, current.node)) { + newKeyword = current.text + } else { + newKeyword = getKeywordNewText(current) + } } SqlTypes.LEFT_PAREN -> { - newKeyword = getNewLineLeftParenString(it.prevSibling, getUpperText(it)) + newKeyword = getNewLineLeftParenString(current.prevSibling, getUpperText(current)) } SqlTypes.RIGHT_PAREN -> { newKeyword = - getRightPatternNewText(it) + getRightPatternNewText(current) } SqlTypes.WORD, SqlTypes.FUNCTION_NAME -> { - newKeyword = getWordNewText(it, newKeyword) + newKeyword = getWordNewText(current, newKeyword) } SqlTypes.COMMA, SqlTypes.OTHER -> { - newKeyword = getNewLineString(it.prevSibling, getUpperText(it)) + newKeyword = getNewLineString(current.prevSibling, getUpperText(current)) } SqlTypes.BLOCK_COMMENT_START -> { newKeyword = - getNewLineString(PsiTreeUtil.prevLeaf(it), getUpperText(it)) + getNewLineString(PsiTreeUtil.prevLeaf(current), getUpperText(current)) } } document.deleteString(textRangeStart, textRangeEnd) document.insertString(textRangeStart, newKeyword) } else { - removeSpacesAroundNewline(document, it as PsiWhiteSpace) + removeSpacesAroundNewline(document, current as PsiWhiteSpace) } } @@ -188,6 +195,29 @@ class SqlFormatPreProcessor : PreFormatProcessor { document.replaceString(range.startOffset, range.endOffset, newText) } + private fun hasEscapeBeforeWhiteSpace( + prevBlocks: List, + start: ASTNode, + ): Boolean { + val countEscape = prevBlocks.filter { it.elementType == SqlTypes.OTHER && it.text in listOf("\"", "[", "`", "]") } + if (countEscape.count() % 2 == 0) { + return false + } + var node = start.treeNext + while (node != null) { + if (node.elementType == SqlTypes.OTHER && + listOf("\"", "`", "]").contains(node.text) + ) { + return true + } + if (node.psi is PsiWhiteSpace) { + return false + } + node = node.treeNext + } + return false + } + /** * Checks for special case keyword elements and specific combinations of keywords with line breaks and capitalization only */ @@ -254,6 +284,7 @@ class SqlFormatPreProcessor : PreFormatProcessor { text: String, ): String = if (prevElement?.elementType == SqlTypes.BLOCK_COMMENT || + prevElement?.elementType == SqlTypes.BLOCK_COMMENT_END || ( prevElement?.text?.contains(LINE_SEPARATE) == false && prevElement.prevSibling != null diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlBlockGenerator.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlBlockGenerator.kt index f12ef23b..4b002d4b 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlBlockGenerator.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlBlockGenerator.kt @@ -22,7 +22,11 @@ import com.intellij.formatting.SpacingBuilder import com.intellij.formatting.Wrap import com.intellij.lang.ASTNode import com.intellij.psi.PsiComment +import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import com.intellij.psi.util.nextLeaf +import com.intellij.psi.util.nextLeafs import org.domaframework.doma.intellij.extension.expr.isConditionOrLoopDirective import org.domaframework.doma.intellij.formatter.block.SqlBlock import org.domaframework.doma.intellij.formatter.block.SqlKeywordBlock @@ -57,6 +61,7 @@ import org.domaframework.doma.intellij.formatter.block.group.keyword.with.SqlWit import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlDataTypeParamBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlSubGroupBlock import org.domaframework.doma.intellij.formatter.block.group.subgroup.SqlSubQueryGroupBlock +import org.domaframework.doma.intellij.formatter.block.other.SqlEscapeBlock import org.domaframework.doma.intellij.formatter.block.word.SqlAliasBlock import org.domaframework.doma.intellij.formatter.block.word.SqlArrayWordBlock import org.domaframework.doma.intellij.formatter.block.word.SqlFunctionGroupBlock @@ -102,6 +107,7 @@ class SqlBlockGenerator( lastGroupBlock: SqlBlock?, ): SqlBlock { val keywordText = child.text.lowercase() + val indentLevel = SqlKeywordUtil.getIndentType(keywordText) if (indentLevel.isNewLineGroup()) { @@ -384,6 +390,47 @@ class SqlBlockGenerator( return CommaRawClauseHandler.getCommaBlock(lastGroup, child, sqlBlockFormattingCtx) } + fun hasEscapeBeforeWhiteSpace( + lastEscapeBlock: SqlBlock?, + start: ASTNode, + ): Boolean { + if (lastEscapeBlock == null || + (lastEscapeBlock as? SqlEscapeBlock)?.isEndEscape == true + ) { + return false + } + var node = start.treeNext + while (node != null) { + if (node.elementType == SqlTypes.OTHER && + listOf("\"", "`", "]").contains(node.text) + ) { + return true + } + if (node.psi is PsiWhiteSpace) { + return false + } + node = node.treeNext + } + return false + } + + fun getFunctionName( + child: ASTNode, + defaultFormatCtx: SqlBlockFormattingContext, + ): SqlBlock? { + val notWhiteSpaceElement = + child.psi.nextLeafs + .takeWhile { it is PsiWhiteSpace } + .lastOrNull() + ?.nextLeaf(true) + if (notWhiteSpaceElement?.elementType == SqlTypes.LEFT_PAREN || + PsiTreeUtil.nextLeaf(child.psi)?.elementType == SqlTypes.LEFT_PAREN + ) { + return SqlFunctionGroupBlock(child, defaultFormatCtx) + } + return null + } + fun getWordBlock( lastGroup: SqlBlock?, child: ASTNode, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlKeywordBlockFactory.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlKeywordBlockFactory.kt index 926db5d4..30f7bbd6 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlKeywordBlockFactory.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/util/SqlKeywordBlockFactory.kt @@ -23,6 +23,8 @@ import org.domaframework.doma.intellij.formatter.block.conflict.OnConflictKeywor import org.domaframework.doma.intellij.formatter.block.conflict.SqlConflictClauseBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.condition.SqlConditionKeywordGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateKeywordGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateTableColumnDefinitionGroupBlock +import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateTableColumnDefinitionRawGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.create.SqlCreateViewGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.inline.SqlInlineGroupBlock import org.domaframework.doma.intellij.formatter.block.group.keyword.option.SqlExistsGroupBlock @@ -123,6 +125,8 @@ class SqlKeywordBlockFactory( val shouldCreateExistsGroup = when { lastGroupBlock is SqlElConditionLoopCommentBlock && lastGroupBlock.conditionType.isElse() -> true + lastGroupBlock is SqlCreateTableColumnDefinitionGroupBlock || + lastGroupBlock is SqlCreateTableColumnDefinitionRawGroupBlock -> false keywordText == "not" && !isInConditionContext(lastGroupBlock) -> true else -> false } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/SqlFormatVisitor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/SqlFormatVisitor.kt index 8fbbde0e..1788c3e4 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/SqlFormatVisitor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/SqlFormatVisitor.kt @@ -42,6 +42,11 @@ class SqlFormatVisitor : PsiRecursiveElementVisitor() { } if (PsiTreeUtil.getParentOfType(element, SqlBlockComment::class.java) == null) { + val prevElement = PsiTreeUtil.prevLeaf(element) + if (prevElement.elementType == SqlTypes.BLOCK_COMMENT_END && element !is PsiWhiteSpace) { + replaces.add(element) + return + } when (element.elementType) { SqlTypes.KEYWORD, SqlTypes.COMMA, SqlTypes.LEFT_PAREN, SqlTypes.RIGHT_PAREN, SqlTypes.WORD, SqlTypes.FUNCTION_NAME -> { replaces.add(element) diff --git a/src/test/kotlin/org/domaframework/doma/intellij/formatter/SqlFormatterTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/formatter/SqlFormatterTest.kt index d068f59e..078c4d7a 100644 --- a/src/test/kotlin/org/domaframework/doma/intellij/formatter/SqlFormatterTest.kt +++ b/src/test/kotlin/org/domaframework/doma/intellij/formatter/SqlFormatterTest.kt @@ -262,6 +262,10 @@ class SqlFormatterTest : BasePlatformTestCase() { formatSqlFile("FunctionKeywordInConditionDirective.sql", "FunctionKeywordInConditionDirective$formatDataPrefix.sql") } + fun testFunctionNameColumnFormatter() { + formatSqlFile("FunctionNameColumn.sql", "FunctionNameColumn$formatDataPrefix.sql") + } + private fun formatSqlFile( beforeFile: String, afterFile: String, diff --git a/src/test/testData/sql/formatter/CreateTable.sql b/src/test/testData/sql/formatter/CreateTable.sql index b90e7bd4..bd7a0f1c 100644 --- a/src/test/testData/sql/formatter/CreateTable.sql +++ b/src/test/testData/sql/formatter/CreateTable.sql @@ -3,4 +3,7 @@ CREATE TABLE departments id INT PRIMARY KEY , name VARCHAR(100) , loc INT NOT NULL + , age INTEGER + , [abs] INTEGER + , "order" VARCHAR(100) ) diff --git a/src/test/testData/sql/formatter/CreateTable_format.sql b/src/test/testData/sql/formatter/CreateTable_format.sql index 280d97e3..e75fd3c5 100644 --- a/src/test/testData/sql/formatter/CreateTable_format.sql +++ b/src/test/testData/sql/formatter/CreateTable_format.sql @@ -1,6 +1,9 @@ CREATE TABLE departments ( - id INT PRIMARY KEY - , name VARCHAR(100) - , loc INT NOT NULL + id INT PRIMARY KEY + , name VARCHAR(100) + , loc INT NOT NULL + , age INTEGER + , [abs] INTEGER + , "order" VARCHAR(100) ) diff --git a/src/test/testData/sql/formatter/FunctionNameColumn.sql b/src/test/testData/sql/formatter/FunctionNameColumn.sql new file mode 100644 index 00000000..55b306e4 --- /dev/null +++ b/src/test/testData/sql/formatter/FunctionNameColumn.sql @@ -0,0 +1,15 @@ +WITH [abs] AS ( +insert into employee(id, name, "left") +values (1, 'name', select "age" from user where id = 1)) + +SELECT id, [age] + , "age" + FROM "order"o, `age` + WHERE (/*%for age : ages */ + "age" = /* age */30 + AND o."Left" = /* left */30 + /*%if age_has_next */ +/*# "and" */ + /*%else */ + /*%end */ + /*%end */) diff --git a/src/test/testData/sql/formatter/FunctionNameColumn_format.sql b/src/test/testData/sql/formatter/FunctionNameColumn_format.sql new file mode 100644 index 00000000..593ce486 --- /dev/null +++ b/src/test/testData/sql/formatter/FunctionNameColumn_format.sql @@ -0,0 +1,24 @@ +WITH [abs] AS ( + INSERT INTO employee + (id + , name + , "left") + VALUES ( 1 + , 'name' + , SELECT "age" + FROM user + WHERE id = 1 ) +) +SELECT id + , [age] + , "age" + FROM "order" o + , `age` + WHERE (/*%for age : ages */ + "age" = /* age */30 + AND o."Left" = /* left */30 + /*%if age_has_next */ + /*# "and" */ + /*%else */ + /*%end */ + /*%end */) diff --git a/src/test/testData/src/main/java/doma/example/dao/sqltoannotation/ScriptWithSqlFileDao.after.java b/src/test/testData/src/main/java/doma/example/dao/sqltoannotation/ScriptWithSqlFileDao.after.java index d0e1de7a..9b4ff357 100644 --- a/src/test/testData/src/main/java/doma/example/dao/sqltoannotation/ScriptWithSqlFileDao.after.java +++ b/src/test/testData/src/main/java/doma/example/dao/sqltoannotation/ScriptWithSqlFileDao.after.java @@ -12,7 +12,7 @@ public interface ScriptWithSqlFileDao { ( id INTEGER PRIMARY KEY , name VARCHAR(100) - , age INTEGER + , age INTEGER ) ; CREATE INDEX idx_employee_name ON employee(name) ;