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/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) { 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/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/SqlFormatPreProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlFormatPreProcessor.kt index aa4b8351..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 @@ -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,10 @@ class SqlFormatPreProcessor : PreFormatProcessor { prevElement: PsiElement?, text: String, ): String = - if (prevElement?.text?.contains("\n") == false) { - "\n$text" + if (prevElement?.text?.contains(StringUtil.LINE_SEPARATE) == false && + PsiTreeUtil.prevLeaf(prevElement) != null + ) { + "${StringUtil.LINE_SEPARATE}$text" } else { text } 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..328e7ba1 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt @@ -0,0 +1,52 @@ +/* + * 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 org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.formatter.visitor.DaoInjectionSqlVisitor + +class SqlInjectionPostProcessor : SqlPostProcessor() { + override fun processElement( + element: PsiElement, + settings: CodeStyleSettings, + ): PsiElement = element + + override fun processText( + source: PsiFile, + rangeToReformat: TextRange, + settings: CodeStyleSettings, + ): TextRange { + if (!isJavaOrKotlinFileType(source) || getDaoClass(source) == null) 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 { 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 2f2a2dfe..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,72 +15,45 @@ */ package org.domaframework.doma.intellij.formatter.processor -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.command.WriteCommandAction -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.setting.SqlLanguage +import org.domaframework.doma.intellij.common.util.StringUtil -class SqlPostProcessor : PostFormatProcessor { +abstract 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)") 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)) { - return rangeToReformat - } + ): TextRange = rangeToReformat - val document = getDocument(source) ?: return rangeToReformat - val processedText = processDocumentText(document.text) - - 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(originalText: String): String { + protected 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 updateDocument( - project: Project, - document: com.intellij.openapi.editor.Document, - newText: String, - ) { - ApplicationManager.getApplication().invokeAndWait { - WriteCommandAction.runWriteCommandAction(project) { - document.setText(newText) - PsiDocumentManager.getInstance(project).commitDocument(document) - } - } - } + private fun ensureProperFileEnding( + text: String, + isEndSpace: Boolean, + ): String = + text.trimEnd() + + if (isEndSpace) FILE_END_PADDING else "" } 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..2b1651e8 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt @@ -0,0 +1,223 @@ +/* + * 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.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 +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, +) : JavaRecursiveElementVisitor() { + 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() + + 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 originalSqlText = injected.text + val formattedSql = formatAsTemporarySqlFile(originalSqlText) + // Keep the current top line indent + val baseIndent = getBaseIndent(formattedSql) + val originalText = expression.value?.toString() ?: return + + 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, 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, removeSpace) + } + } + }) + } + + /** + * 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 { + val tempFileName = "${TEMP_FILE_PREFIX}${SQL_FILE_EXTENSION}" + val fileType = FileTypeManager.getInstance().getFileTypeByExtension("sql") + + val tempSqlFile = + PsiFileFactory + .getInstance(project) + .createFileFromText(tempFileName, fileType, sqlText) + + CodeStyleManager + .getInstance(project) + .reformatText(tempSqlFile, 0, tempSqlFile.textLength) + + tempSqlFile.text + } catch (_: Exception) { + sqlText // Return original text on error + } + + /** + * Replaces the host Java string literal with formatted SQL text. + */ + private fun replaceHostStringLiteral( + task: FormattingTask, + removeSpace: (String, Boolean) -> String, + ) { + try { + val formattedLiteral = createFormattedLiteral(task, removeSpace) + replaceInDocument(task.expression, formattedLiteral) + } catch (_: Exception) { + // Silently ignore formatting failures + } + } + + 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 + } + + 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) + } + + /** + * 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 buildString { + append(TRIPLE_QUOTE) + append(StringUtil.LINE_SEPARATE) + append(lines.joinToString(StringUtil.LINE_SEPARATE)) + append(TRIPLE_QUOTE) + } + } + + /** + * Normalizes indentation by removing base indent and reapplying it consistently. + */ + private fun normalizeIndentation( + sqlText: String, + baseIndent: String, + ): String { + val lines = sqlText.lines() + if (lines.isEmpty()) return sqlText + + val literalSeparator = lines.first() + val contentLines = lines.drop(1) + + 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)) + } + } + } +} 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, + ) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b75e69e6..02d38209 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -66,7 +66,8 @@ - + +