Skip to content

Commit db2e802

Browse files
committed
Align indentation within the injection range to match the position of the first line in that range.
1 parent b15de24 commit db2e802

File tree

1 file changed

+116
-55
lines changed

1 file changed

+116
-55
lines changed

src/main/kotlin/org/domaframework/doma/intellij/formatter/visitor/DaoInjectionSqlVisitor.kt

Lines changed: 116 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package org.domaframework.doma.intellij.formatter.visitor
1818
import com.intellij.openapi.command.WriteCommandAction
1919
import com.intellij.openapi.fileTypes.FileTypeManager
2020
import com.intellij.openapi.project.Project
21-
import com.intellij.openapi.util.TextRange
2221
import com.intellij.psi.JavaRecursiveElementVisitor
2322
import com.intellij.psi.PsiDocumentManager
2423
import com.intellij.psi.PsiFile
@@ -27,19 +26,30 @@ import com.intellij.psi.PsiLiteralExpression
2726
import com.intellij.psi.codeStyle.CodeStyleManager
2827
import org.domaframework.doma.intellij.common.util.InjectionSqlUtil
2928
import org.domaframework.doma.intellij.common.util.StringUtil
29+
import kotlin.text.isBlank
30+
import kotlin.text.isNotBlank
31+
import kotlin.text.takeWhile
3032

33+
/**
34+
* Visitor for processing and formatting SQL injections in DAO files.
35+
* Formats SQL strings embedded in Java/Kotlin string literals while preserving indentation.
36+
*/
3137
class DaoInjectionSqlVisitor(
3238
private val element: PsiFile,
3339
private val project: Project,
3440
) : JavaRecursiveElementVisitor() {
3541
private data class FormattingTask(
3642
val expression: PsiLiteralExpression,
3743
val formattedText: String,
44+
val baseIndent: String,
3845
)
3946

4047
companion object {
4148
private const val TEMP_FILE_PREFIX = "temp_format"
4249
private const val SQL_FILE_EXTENSION = ".sql"
50+
private const val SQL_COMMENT_PATTERN = "^[ \\t]*/\\*"
51+
private const val TRIPLE_QUOTE = "\"\"\""
52+
private const val WRITE_COMMAND_NAME = "Format Injected SQL"
4353
}
4454

4555
private val formattingTasks = mutableListOf<FormattingTask>()
@@ -50,33 +60,60 @@ class DaoInjectionSqlVisitor(
5060
if (injected != null) {
5161
// Format SQL and store the task
5262
val originalSqlText = injected.text
53-
val normalizedSql = normalizeIndentation(originalSqlText)
54-
val formattedSql = formatAsTemporarySqlFile(normalizedSql)
55-
val finalSql = reapplyOriginalIndentation(formattedSql, originalSqlText)
56-
63+
val formattedSql = formatAsTemporarySqlFile(originalSqlText)
64+
// Keep the current top line indent
65+
val baseIndent = getBaseIndent(formattedSql)
5766
val originalText = expression.value?.toString() ?: return
58-
if (finalSql != originalText) {
59-
formattingTasks.add(FormattingTask(expression, finalSql))
67+
68+
if (formattedSql != originalText) {
69+
formattingTasks.add(FormattingTask(expression, formattedSql, baseIndent))
70+
}
71+
}
72+
}
73+
74+
/**
75+
* Extracts the base indentation from the first non-blank, non-comment line.
76+
*/
77+
private fun getBaseIndent(string: String): String {
78+
val lines = string.lines()
79+
val commentRegex = Regex(SQL_COMMENT_PATTERN)
80+
81+
// Skip blank lines and comment lines
82+
val firstContentLineIndex =
83+
lines.indexOfFirst { line ->
84+
line.isNotBlank() && !commentRegex.matches(line)
6085
}
86+
87+
return if (firstContentLineIndex >= 0) {
88+
lines[firstContentLineIndex].takeWhile { it.isWhitespace() }
89+
} else {
90+
""
6191
}
6292
}
6393

94+
/**
95+
* Processes all collected formatting tasks in a single write action.
96+
* @param removeSpace Function to remove trailing spaces from formatted text
97+
*/
6498
fun processAll(removeSpace: (String, Boolean) -> String) {
6599
if (formattingTasks.isEmpty()) return
66100

67101
// Apply all formatting tasks in a single write action
68-
WriteCommandAction.runWriteCommandAction(project, "Format Injected SQL", null, {
102+
WriteCommandAction.runWriteCommandAction(project, WRITE_COMMAND_NAME, null, {
69103
// Sort by text range in descending order to maintain offsets
70-
formattingTasks.sortedByDescending { it.expression.textRange.startOffset }.forEach { task ->
71-
if (task.expression.isValid) {
72-
replaceHostStringLiteral(task.expression, task.formattedText, removeSpace)
104+
formattingTasks
105+
.sortedByDescending { it.expression.textRange.startOffset }
106+
.forEach { task ->
107+
if (task.expression.isValid) {
108+
replaceHostStringLiteral(task, removeSpace)
109+
}
73110
}
74-
}
75111
})
76112
}
77113

78114
/**
79-
* Execute formatting as a temporary SQL file
115+
* Formats SQL text by creating a temporary SQL file and applying code style.
116+
* Returns original text if formatting fails.
80117
*/
81118
private fun formatAsTemporarySqlFile(sqlText: String): String =
82119
try {
@@ -88,75 +125,99 @@ class DaoInjectionSqlVisitor(
88125
.getInstance(project)
89126
.createFileFromText(tempFileName, fileType, sqlText)
90127

91-
val codeStyleManager = CodeStyleManager.getInstance(project)
92-
val textRange = TextRange(0, tempSqlFile.textLength)
93-
codeStyleManager.reformatText(tempSqlFile, textRange.startOffset, textRange.endOffset)
128+
CodeStyleManager
129+
.getInstance(project)
130+
.reformatText(tempSqlFile, 0, tempSqlFile.textLength)
94131

95132
tempSqlFile.text
96133
} catch (_: Exception) {
97-
sqlText
134+
sqlText // Return original text on error
98135
}
99136

100137
/**
101-
* Directly replace host Java string literal
138+
* Replaces the host Java string literal with formatted SQL text.
102139
*/
103140
private fun replaceHostStringLiteral(
104-
literalExpression: PsiLiteralExpression,
105-
formattedSqlText: String,
141+
task: FormattingTask,
106142
removeSpace: (String, Boolean) -> String,
107143
) {
108144
try {
109-
val normalizedSql = normalizeIndentation(formattedSqlText)
110-
val newLiteralText = createFormattedLiteralText(normalizedSql)
111-
val removeSpaceText = removeSpace(newLiteralText, false)
112-
113-
// Replace PSI element
114-
val elementFactory =
115-
com.intellij.psi.JavaPsiFacade
116-
.getElementFactory(project)
117-
val newLiteral = elementFactory.createExpressionFromText(removeSpaceText, literalExpression)
118-
val manager = PsiDocumentManager.getInstance(literalExpression.project)
119-
val document = manager.getDocument(literalExpression.containingFile) ?: return
120-
document.replaceString(literalExpression.textRange.startOffset, literalExpression.textRange.endOffset, newLiteral.text)
145+
val formattedLiteral = createFormattedLiteral(task, removeSpace)
146+
replaceInDocument(task.expression, formattedLiteral)
121147
} catch (_: Exception) {
122-
// Host literal replacement failed: ${e.message}
148+
// Silently ignore formatting failures
123149
}
124150
}
125151

126-
private fun normalizeIndentation(sqlText: String): String {
127-
val lines = sqlText.lines()
128-
val minIndent =
129-
lines
130-
.filter { it.isNotBlank() }
131-
.minOfOrNull { it.indexOfFirst { char -> !char.isWhitespace() } } ?: 0
152+
private fun createFormattedLiteral(
153+
task: FormattingTask,
154+
removeSpace: (String, Boolean) -> String,
155+
): String {
156+
val newLiteralText = createFormattedLiteralText(task.formattedText)
157+
val normalizedText = normalizeIndentation(newLiteralText, task.baseIndent)
158+
val cleanedText = removeSpace(normalizedText, false)
159+
160+
val elementFactory =
161+
com.intellij.psi.JavaPsiFacade
162+
.getElementFactory(project)
163+
val newLiteral = elementFactory.createExpressionFromText(cleanedText, task.expression)
164+
return newLiteral.text
165+
}
132166

133-
return lines.joinToString(StringUtil.LINE_SEPARATE) { line ->
134-
if (line.isBlank()) line else line.drop(minIndent)
135-
}
167+
private fun replaceInDocument(
168+
expression: PsiLiteralExpression,
169+
newText: String,
170+
) {
171+
val document =
172+
PsiDocumentManager
173+
.getInstance(project)
174+
.getDocument(expression.containingFile) ?: return
175+
176+
val range = expression.textRange
177+
document.replaceString(range.startOffset, range.endOffset, newText)
136178
}
137179

138180
/**
139-
* Create appropriate Java string literal from formatted SQL
181+
* Creates a Java text block (triple-quoted string) from formatted SQL.
140182
*/
141183
private fun createFormattedLiteralText(formattedSqlText: String): String {
142184
val lines = formattedSqlText.split(StringUtil.LINE_SEPARATE)
143-
return "\"\"\"${StringUtil.LINE_SEPARATE}${lines.joinToString(StringUtil.LINE_SEPARATE)}\"\"\""
185+
return buildString {
186+
append(TRIPLE_QUOTE)
187+
append(StringUtil.LINE_SEPARATE)
188+
append(lines.joinToString(StringUtil.LINE_SEPARATE))
189+
append(TRIPLE_QUOTE)
190+
}
144191
}
145192

146-
private fun reapplyOriginalIndentation(
147-
formattedSql: String,
148-
originalSql: String,
193+
/**
194+
* Normalizes indentation by removing base indent and reapplying it consistently.
195+
*/
196+
private fun normalizeIndentation(
197+
sqlText: String,
198+
baseIndent: String,
149199
): String {
150-
val originalLines = originalSql.lines()
151-
val formattedLines = formattedSql.lines()
200+
val lines = sqlText.lines()
201+
if (lines.isEmpty()) return sqlText
152202

153-
val originalIndent =
154-
originalLines
155-
.firstOrNull { it.isNotBlank() }
156-
?.takeWhile { it.isWhitespace() } ?: ""
203+
val literalSeparator = lines.first()
204+
val contentLines = lines.drop(1)
157205

158-
return formattedLines.joinToString(StringUtil.LINE_SEPARATE) { line ->
159-
if (line.isBlank()) line else originalIndent + line
206+
val normalizedLines =
207+
contentLines.map { line ->
208+
when {
209+
line.isBlank() -> line
210+
line.startsWith(baseIndent) -> baseIndent + line.removePrefix(baseIndent)
211+
else -> baseIndent + line
212+
}
213+
}
214+
215+
return buildString {
216+
append(literalSeparator)
217+
if (normalizedLines.isNotEmpty()) {
218+
append(StringUtil.LINE_SEPARATE)
219+
append(normalizedLines.joinToString(StringUtil.LINE_SEPARATE))
220+
}
160221
}
161222
}
162223
}

0 commit comments

Comments
 (0)