From a461bcf90452d0a8fce8faf88b214dd40cb8f330 Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 09:39:14 +0900 Subject: [PATCH 1/8] Move the logic for retrieving injected files to a utility class. --- .../doma/intellij/common/psi/PsiDaoMethod.kt | 2 - .../intellij/common/util/InjectionSqlUtil.kt | 44 +++++++++++++++++ .../processor/SqlFormatPreProcessor.kt | 48 +++++++++++-------- .../formatter/processor/SqlPostProcessor.kt | 29 +++++++---- .../inspection/sql/visitor/SqlVisitorBase.kt | 20 +++----- 5 files changed, 99 insertions(+), 44 deletions(-) create mode 100644 src/main/kotlin/org/domaframework/doma/intellij/common/util/InjectionSqlUtil.kt diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt index 74e6ee70..c1933142 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt @@ -16,7 +16,6 @@ package org.domaframework.doma.intellij.common.psi import com.intellij.lang.Language -import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileEditor.FileEditorManager @@ -150,7 +149,6 @@ class PsiDaoMethod( } // the injection part as a custom language file getSqlAnnotation()?.let { annotation -> - InjectedLanguageManager.getInstance(psiProject) annotation.parameterList.children .firstOrNull { it is PsiNameValuePair } ?.let { sql -> diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/InjectionSqlUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/InjectionSqlUtil.kt new file mode 100644 index 00000000..8169ac28 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/InjectionSqlUtil.kt @@ -0,0 +1,44 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.util + +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiLiteralExpression +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType + +object InjectionSqlUtil { + fun initInjectionElement( + basePsiFile: PsiFile, + project: Project, + literal: PsiLiteralExpression, + ): PsiFile? = + if (isJavaOrKotlinFileType(basePsiFile)) { + val injectedLanguageManager = InjectedLanguageManager.getInstance(project) + injectedLanguageManager + .getInjectedPsiFiles(literal) + ?.firstOrNull() + ?.first as? PsiFile + } else { + null + } + + fun isInjectedSqlFile(source: PsiFile): Boolean { + val injectedLanguageManager = InjectedLanguageManager.getInstance(source.project) + return injectedLanguageManager.isInjectedFragment(source) + } +} 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 aa4b8351..50d2b7a4 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 @@ -26,7 +26,9 @@ 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 org.domaframework.doma.intellij.common.util.InjectionSqlUtil.isInjectedSqlFile import org.domaframework.doma.intellij.common.util.PluginLoggerUtil +import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.formatter.util.CreateQueryType import org.domaframework.doma.intellij.formatter.util.SqlKeywordUtil import org.domaframework.doma.intellij.formatter.visitor.SqlFormatVisitor @@ -58,7 +60,10 @@ class SqlFormatPreProcessor : PreFormatProcessor { rangeToReformat: TextRange, ): TextRange { // Turn on by default the code formatter that only runs when explicitly invoked by the user. - if (source.language != SqlLanguage.INSTANCE) return rangeToReformat + // Handle both direct SQL files and injected SQL in Java files + if (source.language != SqlLanguage.INSTANCE && !isInjectedSqlFile(source)) { + return rangeToReformat + } logging() @@ -151,7 +156,6 @@ class SqlFormatPreProcessor : PreFormatProcessor { element: PsiWhiteSpace, ) { val singleSpace = " " - val newLine = "\n" val range = element.textRange val originalText = document.getText(range) val nextElement = element.nextSibling @@ -162,24 +166,28 @@ class SqlFormatPreProcessor : PreFormatProcessor { newText = originalText.replace(originalText, singleSpace) } else { newText = - when (nextElement.elementType) { - SqlTypes.LINE_COMMENT -> { - if (nextElementText.startsWith(newLine)) { - originalText.replace(originalText, singleSpace) - } else if (originalText.contains(newLine)) { - originalText.replace(Regex("\\s*\\n\\s*"), newLine) - } else { - originalText.replace(originalText, singleSpace) + if (element.prevSibling == null) { + "" + } else { + when (nextElement.elementType) { + SqlTypes.LINE_COMMENT -> { + if (nextElementText.startsWith(StringUtil.LINE_SEPARATE)) { + originalText.replace(originalText, singleSpace) + } else if (originalText.contains(StringUtil.LINE_SEPARATE)) { + originalText.replace(Regex("\\s*\\n\\s*"), StringUtil.LINE_SEPARATE) + } else { + originalText.replace(originalText, singleSpace) + } } - } - else -> { - if (nextElementText.contains(newLine) == true) { - originalText.replace(originalText, singleSpace) - } else if (originalText.contains(newLine)) { - originalText.replace(Regex("\\s*\\n\\s*"), newLine) - } else { - originalText.replace(originalText, newLine) + else -> { + if (nextElementText.contains(StringUtil.LINE_SEPARATE) == true) { + originalText.replace(originalText, singleSpace) + } else if (originalText.contains(StringUtil.LINE_SEPARATE)) { + originalText.replace(Regex("\\s*\\n\\s*"), StringUtil.LINE_SEPARATE) + } else { + originalText.replace(originalText, StringUtil.LINE_SEPARATE) + } } } } @@ -264,8 +272,8 @@ class SqlFormatPreProcessor : PreFormatProcessor { prevElement: PsiElement?, text: String, ): String = - if (prevElement?.text?.contains("\n") == false) { - "\n$text" + if (prevElement?.text?.contains(StringUtil.LINE_SEPARATE) == false) { + "${StringUtil.LINE_SEPARATE}$text" } else { text } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt index 2f2a2dfe..a6981311 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt @@ -15,8 +15,10 @@ */ package org.domaframework.doma.intellij.formatter.processor +import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager @@ -24,11 +26,13 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor +import org.domaframework.doma.intellij.common.util.InjectionSqlUtil.isInjectedSqlFile +import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.setting.SqlLanguage class SqlPostProcessor : PostFormatProcessor { companion object { - private const val FILE_END_PADDING = " \n" + private const val FILE_END_PADDING = " ${StringUtil.LINE_SEPARATE}" } private val trailingSpacesRegex = Regex(" +(\r?\n)") @@ -43,12 +47,13 @@ class SqlPostProcessor : PostFormatProcessor { rangeToReformat: TextRange, settings: CodeStyleSettings, ): TextRange { - if (!isSqlFile(source)) { + if (!isSqlFile(source) || isInjectedSqlFile(source)) { return rangeToReformat } - val document = getDocument(source) ?: return rangeToReformat - val processedText = processDocumentText(document.text) + val originalDocument = getDocument(source) + val document = originalDocument ?: source.fileDocument + val processedText = processDocumentText(document.text, originalDocument != null) if (document.text == processedText) { return rangeToReformat @@ -62,18 +67,26 @@ class SqlPostProcessor : PostFormatProcessor { private fun getDocument(source: PsiFile) = PsiDocumentManager.getInstance(source.project).getDocument(source) - private fun processDocumentText(originalText: String): String { + private fun processDocumentText( + originalText: String, + existsOriginalDocument: Boolean, + ): String { val withoutTrailingSpaces = removeTrailingSpaces(originalText) - return ensureProperFileEnding(withoutTrailingSpaces) + return ensureProperFileEnding(withoutTrailingSpaces, existsOriginalDocument) } private fun removeTrailingSpaces(text: String): String = text.replace(trailingSpacesRegex, "$1") - private fun ensureProperFileEnding(text: String): String = text.trimEnd() + FILE_END_PADDING + private fun ensureProperFileEnding( + text: String, + isEndSpace: Boolean, + ): String = + text.trimEnd() + + if (isEndSpace) FILE_END_PADDING else "" private fun updateDocument( project: Project, - document: com.intellij.openapi.editor.Document, + document: Document, newText: String, ) { ApplicationManager.getApplication().invokeAndWait { diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlVisitorBase.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlVisitorBase.kt index 016676ca..18053b4c 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlVisitorBase.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlVisitorBase.kt @@ -15,11 +15,10 @@ */ package org.domaframework.doma.intellij.inspection.sql.visitor -import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import com.intellij.psi.PsiLiteralExpression -import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.util.InjectionSqlUtil import org.domaframework.doma.intellij.psi.SqlVisitor open class SqlVisitorBase : SqlVisitor() { @@ -31,16 +30,9 @@ open class SqlVisitorBase : SqlVisitor() { project: Project, literal: PsiLiteralExpression, ): PsiFile? = - when (isJavaOrKotlinFileType(basePsiFile)) { - true -> { - val injectedLanguageManager = - InjectedLanguageManager.getInstance(project) - injectedLanguageManager - .getInjectedPsiFiles(literal) - ?.firstOrNull() - ?.first as? PsiFile - } - - false -> null - } + InjectionSqlUtil.initInjectionElement( + basePsiFile, + project, + literal, + ) } From dac1f8f031a6eef1cbe7c9e6eae0e10d43e647aa Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 09:41:21 +0900 Subject: [PATCH 2/8] Move the line break character handling to a utility class. --- .../domaframework/doma/intellij/common/util/StringUtil.kt | 2 ++ .../intellij/document/ForItemElementDocumentationProvider.kt | 5 +++-- .../intellij/formatter/block/comment/SqlBlockCommentBlock.kt | 4 +++- .../formatter/block/comment/SqlDefaultCommentBlock.kt | 3 ++- .../block/comment/SqlElConditionLoopCommentBlock.kt | 3 ++- .../intellij/formatter/builder/SqlCustomSpacingBuilder.kt | 5 +++-- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/StringUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/StringUtil.kt index d6f69d5b..bd4499d1 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/util/StringUtil.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/StringUtil.kt @@ -16,6 +16,8 @@ package org.domaframework.doma.intellij.common.util object StringUtil { + const val LINE_SEPARATE: String = "\n" + fun getSqlElClassText(text: String): String = text .substringAfter("@") diff --git a/src/main/kotlin/org/domaframework/doma/intellij/document/ForItemElementDocumentationProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/document/ForItemElementDocumentationProvider.kt index 4ce0034e..f3ac8b0e 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/document/ForItemElementDocumentationProvider.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/document/ForItemElementDocumentationProvider.kt @@ -18,6 +18,7 @@ package org.domaframework.doma.intellij.document import com.intellij.lang.documentation.AbstractDocumentationProvider import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.document.generator.DocumentDaoParameterGenerator import org.domaframework.doma.intellij.document.generator.DocumentStaticFieldGenerator import org.domaframework.doma.intellij.psi.SqlElIdExpr @@ -62,7 +63,7 @@ class ForItemElementDocumentationProvider : AbstractDocumentationProvider() { generator.generateDocument() - return result.joinToString("\n") + return result.joinToString(StringUtil.LINE_SEPARATE) } override fun generateHoverDoc( @@ -77,6 +78,6 @@ class ForItemElementDocumentationProvider : AbstractDocumentationProvider() { val result: MutableList = LinkedList() val typeDocument = generateDoc(element, originalElement) result.add(typeDocument) - return result.joinToString("\n") + return result.joinToString(StringUtil.LINE_SEPARATE) } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlBlockCommentBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlBlockCommentBlock.kt index ead89a35..baac03ab 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlBlockCommentBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlBlockCommentBlock.kt @@ -17,6 +17,7 @@ package org.domaframework.doma.intellij.formatter.block.comment import com.intellij.lang.ASTNode import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.formatter.block.SqlBlock import org.domaframework.doma.intellij.formatter.util.SqlBlockFormattingContext @@ -27,5 +28,6 @@ open class SqlBlockCommentBlock( node, context, ) { - override fun isSaveSpace(lastGroup: SqlBlock?): Boolean = PsiTreeUtil.prevLeaf(node.psi)?.text?.contains("\n") == true + override fun isSaveSpace(lastGroup: SqlBlock?): Boolean = + PsiTreeUtil.prevLeaf(node.psi)?.text?.contains(StringUtil.LINE_SEPARATE) == true } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlDefaultCommentBlock.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlDefaultCommentBlock.kt index 07eba562..1c9047ca 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlDefaultCommentBlock.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/block/comment/SqlDefaultCommentBlock.kt @@ -18,6 +18,7 @@ package org.domaframework.doma.intellij.formatter.block.comment import com.intellij.lang.ASTNode import com.intellij.psi.formatter.common.AbstractBlock import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.formatter.block.SqlBlock import org.domaframework.doma.intellij.formatter.util.IndentType import org.domaframework.doma.intellij.formatter.util.SqlBlockFormattingContext @@ -60,5 +61,5 @@ abstract class SqlDefaultCommentBlock( } } - override fun isSaveSpace(lastGroup: SqlBlock?) = PsiTreeUtil.prevLeaf(node.psi)?.text?.contains("\n") == true + override fun isSaveSpace(lastGroup: SqlBlock?) = PsiTreeUtil.prevLeaf(node.psi)?.text?.contains(StringUtil.LINE_SEPARATE) == true } 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 be4b73fe..301fd1d1 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 @@ -22,6 +22,7 @@ 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 org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.common.util.TypeUtil import org.domaframework.doma.intellij.extension.expr.isConditionOrLoopDirective import org.domaframework.doma.intellij.formatter.block.SqlBlock @@ -108,7 +109,7 @@ class SqlElConditionLoopCommentBlock( // If the child is a condition loop directive, align its indentation with the parent directive child.indent.indentLen = indent.indentLen.plus(2) } else if (child is SqlLineCommentBlock) { - if (PsiTreeUtil.prevLeaf(child.node.psi, false)?.text?.contains("\n") == true) { + if (PsiTreeUtil.prevLeaf(child.node.psi, false)?.text?.contains(StringUtil.LINE_SEPARATE) == true) { child.indent.indentLen = indent.groupIndentLen } else { child.indent.indentLen = 1 diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlCustomSpacingBuilder.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlCustomSpacingBuilder.kt index 41e1a314..ab13ff00 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlCustomSpacingBuilder.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlCustomSpacingBuilder.kt @@ -19,6 +19,7 @@ import com.intellij.formatting.ASTBlock import com.intellij.formatting.Block import com.intellij.formatting.Spacing import com.intellij.psi.tree.IElementType +import org.domaframework.doma.intellij.common.util.StringUtil import org.domaframework.doma.intellij.formatter.block.SqlBlock import org.domaframework.doma.intellij.formatter.block.SqlRightPatternBlock import org.domaframework.doma.intellij.formatter.block.SqlWhitespaceBlock @@ -79,8 +80,8 @@ class SqlCustomSpacingBuilder { null -> return nonSpacing is SqlWhitespaceBlock -> { val indentLen: Int = child2.indent.indentLen - val afterNewLine = child1.getNodeText().substringAfterLast("\n", "") - if (child1.getNodeText().contains("\n")) { + val afterNewLine = child1.getNodeText().substringAfterLast(StringUtil.LINE_SEPARATE, "") + if (child1.getNodeText().contains(StringUtil.LINE_SEPARATE)) { val currentIndent = afterNewLine.length val newIndent = if (currentIndent != indentLen) { From 9214e89d7abfe5e847fda34f9ef292a91ea2d7f2 Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 09:43:15 +0900 Subject: [PATCH 3/8] Apply individual formatting logic to SQL injected via SQL annotations. --- .../builder/SqlFormattingModelBuilder.kt | 32 +++-- .../processor/SqlInjectionPostProcessor.kt | 50 +++++++ .../formatter/processor/SqlPostProcessor.kt | 1 - .../visitor/DaoInjectionSqlVisitor.kt | 135 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 5 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt create mode 100644 src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlFormattingModelBuilder.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlFormattingModelBuilder.kt index 479f9c9f..ca3ea89b 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlFormattingModelBuilder.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/builder/SqlFormattingModelBuilder.kt @@ -38,25 +38,35 @@ import org.domaframework.doma.intellij.setting.state.DomaToolsFormatEnableSettin class SqlFormattingModelBuilder : FormattingModelBuilder { override fun createModel(formattingContext: FormattingContext): FormattingModel { val codeStyleSettings = formattingContext.codeStyleSettings + + return createRegularSQLModel(formattingContext, codeStyleSettings) + } + + private fun createRegularSQLModel( + formattingContext: FormattingContext, + settings: CodeStyleSettings, + ): FormattingModel { val setting = DomaToolsFormatEnableSettings.getInstance() val isEnableFormat = setting.state.isEnableSqlFormat == true val formatMode = formattingContext.formattingMode - val spacingBuilder = createSpaceBuilder(codeStyleSettings) + val spacingBuilder = createSpaceBuilder(settings) val customSpacingBuilder = createCustomSpacingBuilder() + val block = + SqlFileBlock( + formattingContext.node, + Wrap.createWrap(WrapType.NONE, false), + Alignment.createAlignment(), + customSpacingBuilder, + spacingBuilder, + isEnableFormat, + formatMode, + ) return FormattingModelProvider .createFormattingModelForPsiFile( formattingContext.containingFile, - SqlFileBlock( - formattingContext.node, - Wrap.createWrap(WrapType.NONE, false), - Alignment.createAlignment(), - customSpacingBuilder, - spacingBuilder, - isEnableFormat, - formatMode, - ), - codeStyleSettings, + block, + settings, ) } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt new file mode 100644 index 00000000..e68676e7 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt @@ -0,0 +1,50 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.formatter.processor + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.formatter.visitor.DaoInjectionSqlVisitor + +class SqlInjectionPostProcessor : PostFormatProcessor { + override fun processElement( + element: PsiElement, + settings: CodeStyleSettings, + ): PsiElement = element + + override fun processText( + source: PsiFile, + rangeToReformat: TextRange, + settings: CodeStyleSettings, + ): TextRange { + if (!isJavaOrKotlinFileType(source)) return rangeToReformat + + processInjected(source) + return rangeToReformat + } + + private fun processInjected(element: PsiFile) { + val project: Project = element.project + val visitor = DaoInjectionSqlVisitor(element, project) + element.accept(visitor) + visitor.processAll() + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt index a6981311..64d393cb 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt @@ -15,7 +15,6 @@ */ package org.domaframework.doma.intellij.formatter.processor -import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt new file mode 100644 index 00000000..7ed57ac5 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt @@ -0,0 +1,135 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.formatter.visitor + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.JavaRecursiveElementVisitor +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.codeStyle.CodeStyleManager +import org.domaframework.doma.intellij.common.util.InjectionSqlUtil +import org.domaframework.doma.intellij.common.util.StringUtil + +class DaoInjectionSqlVisitor( + private val element: PsiFile, + private val project: Project, +) : JavaRecursiveElementVisitor() { + private data class FormattingTask( + val expression: PsiLiteralExpression, + val formattedText: String, + ) + + companion object { + private const val TEMP_FILE_PREFIX = "temp_format" + private const val SQL_FILE_EXTENSION = ".sql" + } + + private val formattingTasks = mutableListOf() + + override fun visitLiteralExpression(expression: PsiLiteralExpression) { + super.visitLiteralExpression(expression) + val injected: PsiFile? = InjectionSqlUtil.initInjectionElement(element, project, expression) + if (injected != null) { + // Format SQL and store the task + val formattedSql = formatInjectedSql(injected) + val originalText = expression.value?.toString() ?: return + if (formattedSql != originalText) { + formattingTasks.add(FormattingTask(expression, formattedSql)) + } + } + } + + fun processAll() { + if (formattingTasks.isEmpty()) return + + // Apply all formatting tasks in a single write action + WriteCommandAction.runWriteCommandAction(project, "Format Injected SQL", null, { + // Sort by text range in descending order to maintain offsets + formattingTasks.sortedByDescending { it.expression.textRange.startOffset }.forEach { task -> + if (task.expression.isValid) { + replaceHostStringLiteral(task.expression, task.formattedText) + } + } + }) + } + + private fun formatInjectedSql(injectedFile: PsiFile): String = + try { + val originalSqlText = injectedFile.text + formatAsTemporarySqlFile(originalSqlText) + } catch (_: Exception) { + injectedFile.text + } + + /** + * Execute formatting as a temporary SQL file + */ + private fun formatAsTemporarySqlFile(sqlText: String): String = + try { + val tempFileName = "${TEMP_FILE_PREFIX}${SQL_FILE_EXTENSION}" + val fileType = FileTypeManager.getInstance().getFileTypeByExtension("sql") + + val tempSqlFile = + PsiFileFactory + .getInstance(project) + .createFileFromText(tempFileName, fileType, sqlText) + + val codeStyleManager = CodeStyleManager.getInstance(project) + val textRange = TextRange(0, tempSqlFile.textLength) + codeStyleManager.reformatText(tempSqlFile, textRange.startOffset, textRange.endOffset) + + tempSqlFile.text + } catch (_: Exception) { + sqlText + } + + /** + * Directly replace host Java string literal + */ + private fun replaceHostStringLiteral( + literalExpression: PsiLiteralExpression, + formattedSqlText: String, + ) { + try { + // Create new string literal + val newLiteralText = createFormattedLiteralText(formattedSqlText) + + // Replace PSI element + val elementFactory = + com.intellij.psi.JavaPsiFacade + .getElementFactory(project) + val newLiteral = elementFactory.createExpressionFromText(newLiteralText, literalExpression) + val manager = PsiDocumentManager.getInstance(literalExpression.project) + val document = manager.getDocument(literalExpression.containingFile) ?: return + document.replaceString(literalExpression.textRange.startOffset, literalExpression.textRange.endOffset, newLiteral.text) + } catch (_: Exception) { + // Host literal replacement failed: ${e.message} + } + } + + /** + * Create appropriate Java string literal from formatted SQL + */ + private fun createFormattedLiteralText(formattedSqlText: String): String { + val lines = formattedSqlText.split(StringUtil.LINE_SEPARATE) + return "\"\"\"${StringUtil.LINE_SEPARATE}${lines.joinToString(StringUtil.LINE_SEPARATE)}\"\"\"" + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b75e69e6..2878d5c9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -67,6 +67,7 @@ implementationClass="org.domaframework.doma.intellij.formatter.builder.SqlFormattingModelBuilder"/> + Date: Fri, 25 Jul 2025 10:01:04 +0900 Subject: [PATCH 4/8] Enhance SQL injection processing by adding DAO class check for Java/Kotlin files --- .../intellij/formatter/processor/SqlInjectionPostProcessor.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt index e68676e7..834484c3 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt @@ -21,6 +21,7 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor +import org.domaframework.doma.intellij.common.dao.getDaoClass import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType import org.domaframework.doma.intellij.formatter.visitor.DaoInjectionSqlVisitor @@ -35,7 +36,7 @@ class SqlInjectionPostProcessor : PostFormatProcessor { rangeToReformat: TextRange, settings: CodeStyleSettings, ): TextRange { - if (!isJavaOrKotlinFileType(source)) return rangeToReformat + if (!isJavaOrKotlinFileType(source) || getDaoClass(source) == null) return rangeToReformat processInjected(source) return rangeToReformat From 2f48acdc16575f35935e451343bd661a72938f53 Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 11:55:15 +0900 Subject: [PATCH 5/8] Create an abstract class to share trailing whitespace removal logic in the PostProcessor for both regular SQL files and injected SQL. --- .../processor/SqlFormatPostProcessor.kt | 77 +++++++++++++++++++ .../processor/SqlInjectionPostProcessor.kt | 7 +- .../formatter/processor/SqlPostProcessor.kt | 49 ++---------- src/main/resources/META-INF/plugin.xml | 2 +- 4 files changed, 87 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPostProcessor.kt diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPostProcessor.kt new file mode 100644 index 00000000..0b48d736 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPostProcessor.kt @@ -0,0 +1,77 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.formatter.processor + +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleSettings +import org.domaframework.doma.intellij.setting.SqlLanguage + +class SqlFormatPostProcessor : SqlPostProcessor() { + override fun processElement( + source: PsiElement, + settings: CodeStyleSettings, + ): PsiElement = source + + override fun processText( + source: PsiFile, + rangeToReformat: TextRange, + settings: CodeStyleSettings, + ): TextRange { + if (!isSqlFile(source) || isInjectedSqlFile(source)) { + return rangeToReformat + } + + val document = getDocument(source) ?: return rangeToReformat + val processedText = processDocumentText(document.text, true) + + if (document.text == processedText) { + return rangeToReformat + } + + updateDocument(source.project, document, processedText) + return TextRange(0, processedText.length) + } + + private fun isSqlFile(source: PsiFile): Boolean = source.language == SqlLanguage.INSTANCE + + private fun isInjectedSqlFile(source: PsiFile): Boolean { + val injectedLanguageManager = InjectedLanguageManager.getInstance(source.project) + return injectedLanguageManager.isInjectedFragment(source) + } + + private fun getDocument(source: PsiFile) = PsiDocumentManager.getInstance(source.project).getDocument(source) + + private fun updateDocument( + project: Project, + document: Document, + newText: String, + ) { + ApplicationManager.getApplication().invokeAndWait { + WriteCommandAction.runWriteCommandAction(project) { + document.setText(newText) + PsiDocumentManager.getInstance(project).commitDocument(document) + } + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt index 834484c3..328e7ba1 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt @@ -20,12 +20,11 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.codeStyle.CodeStyleSettings -import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor import org.domaframework.doma.intellij.common.dao.getDaoClass import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType import org.domaframework.doma.intellij.formatter.visitor.DaoInjectionSqlVisitor -class SqlInjectionPostProcessor : PostFormatProcessor { +class SqlInjectionPostProcessor : SqlPostProcessor() { override fun processElement( element: PsiElement, settings: CodeStyleSettings, @@ -46,6 +45,8 @@ class SqlInjectionPostProcessor : PostFormatProcessor { val project: Project = element.project val visitor = DaoInjectionSqlVisitor(element, project) element.accept(visitor) - visitor.processAll() + visitor.processAll { text, skipFinalLineBreak -> + processDocumentText(text, skipFinalLineBreak) + } } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt index 64d393cb..5bf3e992 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlPostProcessor.kt @@ -15,21 +15,14 @@ */ package org.domaframework.doma.intellij.formatter.processor -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.Document -import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor -import org.domaframework.doma.intellij.common.util.InjectionSqlUtil.isInjectedSqlFile import org.domaframework.doma.intellij.common.util.StringUtil -import org.domaframework.doma.intellij.setting.SqlLanguage -class SqlPostProcessor : PostFormatProcessor { +abstract class SqlPostProcessor : PostFormatProcessor { companion object { private const val FILE_END_PADDING = " ${StringUtil.LINE_SEPARATE}" } @@ -37,36 +30,17 @@ class SqlPostProcessor : PostFormatProcessor { private val trailingSpacesRegex = Regex(" +(\r?\n)") override fun processElement( - source: PsiElement, + element: PsiElement, settings: CodeStyleSettings, - ): PsiElement = source + ): PsiElement = element override fun processText( source: PsiFile, rangeToReformat: TextRange, settings: CodeStyleSettings, - ): TextRange { - if (!isSqlFile(source) || isInjectedSqlFile(source)) { - return rangeToReformat - } + ): TextRange = rangeToReformat - val originalDocument = getDocument(source) - val document = originalDocument ?: source.fileDocument - val processedText = processDocumentText(document.text, originalDocument != null) - - if (document.text == processedText) { - return rangeToReformat - } - - updateDocument(source.project, document, processedText) - return TextRange(0, processedText.length) - } - - private fun isSqlFile(source: PsiFile): Boolean = source.language == SqlLanguage.INSTANCE - - private fun getDocument(source: PsiFile) = PsiDocumentManager.getInstance(source.project).getDocument(source) - - private fun processDocumentText( + protected fun processDocumentText( originalText: String, existsOriginalDocument: Boolean, ): String { @@ -82,17 +56,4 @@ class SqlPostProcessor : PostFormatProcessor { ): String = text.trimEnd() + if (isEndSpace) FILE_END_PADDING else "" - - private fun updateDocument( - project: Project, - document: Document, - newText: String, - ) { - ApplicationManager.getApplication().invokeAndWait { - WriteCommandAction.runWriteCommandAction(project) { - document.setText(newText) - PsiDocumentManager.getInstance(project).commitDocument(document) - } - } - } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 2878d5c9..02d38209 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -66,7 +66,7 @@ - + From 5df32faf96ec9caa7fa59c988778ce06426ec786 Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 11:56:00 +0900 Subject: [PATCH 6/8] Fix to prevent unnecessary line breaks when there is whitespace before the first element. --- .../intellij/formatter/processor/SqlFormatPreProcessor.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 50d2b7a4..19bb0351 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 @@ -272,7 +272,9 @@ class SqlFormatPreProcessor : PreFormatProcessor { prevElement: PsiElement?, text: String, ): String = - if (prevElement?.text?.contains(StringUtil.LINE_SEPARATE) == false) { + if (prevElement?.text?.contains(StringUtil.LINE_SEPARATE) == false && + PsiTreeUtil.prevLeaf(prevElement) != null + ) { "${StringUtil.LINE_SEPARATE}$text" } else { text From b15de240d2b87da92910490c5fbaf9a68252ace7 Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 11:56:27 +0900 Subject: [PATCH 7/8] Refactor SQL formatting logic to normalize indentation and reapply original formatting --- .../visitor/DaoInjectionSqlVisitor.kt | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt index 7ed57ac5..8dce7f7d 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt @@ -49,15 +49,19 @@ class DaoInjectionSqlVisitor( val injected: PsiFile? = InjectionSqlUtil.initInjectionElement(element, project, expression) if (injected != null) { // Format SQL and store the task - val formattedSql = formatInjectedSql(injected) + val originalSqlText = injected.text + val normalizedSql = normalizeIndentation(originalSqlText) + val formattedSql = formatAsTemporarySqlFile(normalizedSql) + val finalSql = reapplyOriginalIndentation(formattedSql, originalSqlText) + val originalText = expression.value?.toString() ?: return - if (formattedSql != originalText) { - formattingTasks.add(FormattingTask(expression, formattedSql)) + if (finalSql != originalText) { + formattingTasks.add(FormattingTask(expression, finalSql)) } } } - fun processAll() { + fun processAll(removeSpace: (String, Boolean) -> String) { if (formattingTasks.isEmpty()) return // Apply all formatting tasks in a single write action @@ -65,20 +69,12 @@ class DaoInjectionSqlVisitor( // Sort by text range in descending order to maintain offsets formattingTasks.sortedByDescending { it.expression.textRange.startOffset }.forEach { task -> if (task.expression.isValid) { - replaceHostStringLiteral(task.expression, task.formattedText) + replaceHostStringLiteral(task.expression, task.formattedText, removeSpace) } } }) } - private fun formatInjectedSql(injectedFile: PsiFile): String = - try { - val originalSqlText = injectedFile.text - formatAsTemporarySqlFile(originalSqlText) - } catch (_: Exception) { - injectedFile.text - } - /** * Execute formatting as a temporary SQL file */ @@ -107,16 +103,18 @@ class DaoInjectionSqlVisitor( private fun replaceHostStringLiteral( literalExpression: PsiLiteralExpression, formattedSqlText: String, + removeSpace: (String, Boolean) -> String, ) { try { - // Create new string literal - val newLiteralText = createFormattedLiteralText(formattedSqlText) + val normalizedSql = normalizeIndentation(formattedSqlText) + val newLiteralText = createFormattedLiteralText(normalizedSql) + val removeSpaceText = removeSpace(newLiteralText, false) // Replace PSI element val elementFactory = com.intellij.psi.JavaPsiFacade .getElementFactory(project) - val newLiteral = elementFactory.createExpressionFromText(newLiteralText, literalExpression) + val newLiteral = elementFactory.createExpressionFromText(removeSpaceText, literalExpression) val manager = PsiDocumentManager.getInstance(literalExpression.project) val document = manager.getDocument(literalExpression.containingFile) ?: return document.replaceString(literalExpression.textRange.startOffset, literalExpression.textRange.endOffset, newLiteral.text) @@ -125,6 +123,18 @@ class DaoInjectionSqlVisitor( } } + private fun normalizeIndentation(sqlText: String): String { + val lines = sqlText.lines() + val minIndent = + lines + .filter { it.isNotBlank() } + .minOfOrNull { it.indexOfFirst { char -> !char.isWhitespace() } } ?: 0 + + return lines.joinToString(StringUtil.LINE_SEPARATE) { line -> + if (line.isBlank()) line else line.drop(minIndent) + } + } + /** * Create appropriate Java string literal from formatted SQL */ @@ -132,4 +142,21 @@ class DaoInjectionSqlVisitor( val lines = formattedSqlText.split(StringUtil.LINE_SEPARATE) return "\"\"\"${StringUtil.LINE_SEPARATE}${lines.joinToString(StringUtil.LINE_SEPARATE)}\"\"\"" } + + private fun reapplyOriginalIndentation( + formattedSql: String, + originalSql: String, + ): String { + val originalLines = originalSql.lines() + val formattedLines = formattedSql.lines() + + val originalIndent = + originalLines + .firstOrNull { it.isNotBlank() } + ?.takeWhile { it.isWhitespace() } ?: "" + + return formattedLines.joinToString(StringUtil.LINE_SEPARATE) { line -> + if (line.isBlank()) line else originalIndent + line + } + } } From db2e802ea6af2f4955d8b8b83ff0d51ae2cc68a7 Mon Sep 17 00:00:00 2001 From: xterao Date: Fri, 25 Jul 2025 17:53:22 +0900 Subject: [PATCH 8/8] Align indentation within the injection range to match the position of the first line in that range. --- .../visitor/DaoInjectionSqlVisitor.kt | 171 ++++++++++++------ 1 file changed, 116 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt index 8dce7f7d..2b1651e8 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt @@ -18,7 +18,6 @@ package org.domaframework.doma.intellij.formatter.visitor import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaRecursiveElementVisitor import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile @@ -27,7 +26,14 @@ import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.codeStyle.CodeStyleManager import org.domaframework.doma.intellij.common.util.InjectionSqlUtil import org.domaframework.doma.intellij.common.util.StringUtil +import kotlin.text.isBlank +import kotlin.text.isNotBlank +import kotlin.text.takeWhile +/** + * Visitor for processing and formatting SQL injections in DAO files. + * Formats SQL strings embedded in Java/Kotlin string literals while preserving indentation. + */ class DaoInjectionSqlVisitor( private val element: PsiFile, private val project: Project, @@ -35,11 +41,15 @@ class DaoInjectionSqlVisitor( private data class FormattingTask( val expression: PsiLiteralExpression, val formattedText: String, + val baseIndent: String, ) companion object { private const val TEMP_FILE_PREFIX = "temp_format" private const val SQL_FILE_EXTENSION = ".sql" + private const val SQL_COMMENT_PATTERN = "^[ \\t]*/\\*" + private const val TRIPLE_QUOTE = "\"\"\"" + private const val WRITE_COMMAND_NAME = "Format Injected SQL" } private val formattingTasks = mutableListOf() @@ -50,33 +60,60 @@ class DaoInjectionSqlVisitor( if (injected != null) { // Format SQL and store the task val originalSqlText = injected.text - val normalizedSql = normalizeIndentation(originalSqlText) - val formattedSql = formatAsTemporarySqlFile(normalizedSql) - val finalSql = reapplyOriginalIndentation(formattedSql, originalSqlText) - + val formattedSql = formatAsTemporarySqlFile(originalSqlText) + // Keep the current top line indent + val baseIndent = getBaseIndent(formattedSql) val originalText = expression.value?.toString() ?: return - if (finalSql != originalText) { - formattingTasks.add(FormattingTask(expression, finalSql)) + + if (formattedSql != originalText) { + formattingTasks.add(FormattingTask(expression, formattedSql, baseIndent)) + } + } + } + + /** + * Extracts the base indentation from the first non-blank, non-comment line. + */ + private fun getBaseIndent(string: String): String { + val lines = string.lines() + val commentRegex = Regex(SQL_COMMENT_PATTERN) + + // Skip blank lines and comment lines + val firstContentLineIndex = + lines.indexOfFirst { line -> + line.isNotBlank() && !commentRegex.matches(line) } + + return if (firstContentLineIndex >= 0) { + lines[firstContentLineIndex].takeWhile { it.isWhitespace() } + } else { + "" } } + /** + * Processes all collected formatting tasks in a single write action. + * @param removeSpace Function to remove trailing spaces from formatted text + */ fun processAll(removeSpace: (String, Boolean) -> String) { if (formattingTasks.isEmpty()) return // Apply all formatting tasks in a single write action - WriteCommandAction.runWriteCommandAction(project, "Format Injected SQL", null, { + WriteCommandAction.runWriteCommandAction(project, WRITE_COMMAND_NAME, null, { // Sort by text range in descending order to maintain offsets - formattingTasks.sortedByDescending { it.expression.textRange.startOffset }.forEach { task -> - if (task.expression.isValid) { - replaceHostStringLiteral(task.expression, task.formattedText, removeSpace) + formattingTasks + .sortedByDescending { it.expression.textRange.startOffset } + .forEach { task -> + if (task.expression.isValid) { + replaceHostStringLiteral(task, removeSpace) + } } - } }) } /** - * Execute formatting as a temporary SQL file + * Formats SQL text by creating a temporary SQL file and applying code style. + * Returns original text if formatting fails. */ private fun formatAsTemporarySqlFile(sqlText: String): String = try { @@ -88,75 +125,99 @@ class DaoInjectionSqlVisitor( .getInstance(project) .createFileFromText(tempFileName, fileType, sqlText) - val codeStyleManager = CodeStyleManager.getInstance(project) - val textRange = TextRange(0, tempSqlFile.textLength) - codeStyleManager.reformatText(tempSqlFile, textRange.startOffset, textRange.endOffset) + CodeStyleManager + .getInstance(project) + .reformatText(tempSqlFile, 0, tempSqlFile.textLength) tempSqlFile.text } catch (_: Exception) { - sqlText + sqlText // Return original text on error } /** - * Directly replace host Java string literal + * Replaces the host Java string literal with formatted SQL text. */ private fun replaceHostStringLiteral( - literalExpression: PsiLiteralExpression, - formattedSqlText: String, + task: FormattingTask, removeSpace: (String, Boolean) -> String, ) { try { - val normalizedSql = normalizeIndentation(formattedSqlText) - val newLiteralText = createFormattedLiteralText(normalizedSql) - val removeSpaceText = removeSpace(newLiteralText, false) - - // Replace PSI element - val elementFactory = - com.intellij.psi.JavaPsiFacade - .getElementFactory(project) - val newLiteral = elementFactory.createExpressionFromText(removeSpaceText, literalExpression) - val manager = PsiDocumentManager.getInstance(literalExpression.project) - val document = manager.getDocument(literalExpression.containingFile) ?: return - document.replaceString(literalExpression.textRange.startOffset, literalExpression.textRange.endOffset, newLiteral.text) + val formattedLiteral = createFormattedLiteral(task, removeSpace) + replaceInDocument(task.expression, formattedLiteral) } catch (_: Exception) { - // Host literal replacement failed: ${e.message} + // Silently ignore formatting failures } } - private fun normalizeIndentation(sqlText: String): String { - val lines = sqlText.lines() - val minIndent = - lines - .filter { it.isNotBlank() } - .minOfOrNull { it.indexOfFirst { char -> !char.isWhitespace() } } ?: 0 + private fun createFormattedLiteral( + task: FormattingTask, + removeSpace: (String, Boolean) -> String, + ): String { + val newLiteralText = createFormattedLiteralText(task.formattedText) + val normalizedText = normalizeIndentation(newLiteralText, task.baseIndent) + val cleanedText = removeSpace(normalizedText, false) + + val elementFactory = + com.intellij.psi.JavaPsiFacade + .getElementFactory(project) + val newLiteral = elementFactory.createExpressionFromText(cleanedText, task.expression) + return newLiteral.text + } - return lines.joinToString(StringUtil.LINE_SEPARATE) { line -> - if (line.isBlank()) line else line.drop(minIndent) - } + private fun replaceInDocument( + expression: PsiLiteralExpression, + newText: String, + ) { + val document = + PsiDocumentManager + .getInstance(project) + .getDocument(expression.containingFile) ?: return + + val range = expression.textRange + document.replaceString(range.startOffset, range.endOffset, newText) } /** - * Create appropriate Java string literal from formatted SQL + * Creates a Java text block (triple-quoted string) from formatted SQL. */ private fun createFormattedLiteralText(formattedSqlText: String): String { val lines = formattedSqlText.split(StringUtil.LINE_SEPARATE) - return "\"\"\"${StringUtil.LINE_SEPARATE}${lines.joinToString(StringUtil.LINE_SEPARATE)}\"\"\"" + return buildString { + append(TRIPLE_QUOTE) + append(StringUtil.LINE_SEPARATE) + append(lines.joinToString(StringUtil.LINE_SEPARATE)) + append(TRIPLE_QUOTE) + } } - private fun reapplyOriginalIndentation( - formattedSql: String, - originalSql: String, + /** + * Normalizes indentation by removing base indent and reapplying it consistently. + */ + private fun normalizeIndentation( + sqlText: String, + baseIndent: String, ): String { - val originalLines = originalSql.lines() - val formattedLines = formattedSql.lines() + val lines = sqlText.lines() + if (lines.isEmpty()) return sqlText - val originalIndent = - originalLines - .firstOrNull { it.isNotBlank() } - ?.takeWhile { it.isWhitespace() } ?: "" + val literalSeparator = lines.first() + val contentLines = lines.drop(1) - return formattedLines.joinToString(StringUtil.LINE_SEPARATE) { line -> - if (line.isBlank()) line else originalIndent + line + val normalizedLines = + contentLines.map { line -> + when { + line.isBlank() -> line + line.startsWith(baseIndent) -> baseIndent + line.removePrefix(baseIndent) + else -> baseIndent + line + } + } + + return buildString { + append(literalSeparator) + if (normalizedLines.isNotEmpty()) { + append(StringUtil.LINE_SEPARATE) + append(normalizedLines.joinToString(StringUtil.LINE_SEPARATE)) + } } } }