Skip to content

Commit 2185adb

Browse files
committed
Implemented a dedicated class for explicit format processing, allowing invocation outside of the formatter functionality.
1 parent 0302f14 commit 2185adb

File tree

3 files changed

+225
-176
lines changed

3 files changed

+225
-176
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package org.domaframework.doma.intellij.formatter.processor
2+
3+
import com.intellij.lang.injection.InjectedLanguageManager
4+
import com.intellij.openapi.command.WriteCommandAction
5+
import com.intellij.openapi.fileTypes.FileTypeManager
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.psi.PsiDocumentManager
8+
import com.intellij.psi.PsiFile
9+
import com.intellij.psi.PsiFileFactory
10+
import com.intellij.psi.PsiLiteralExpression
11+
import com.intellij.psi.codeStyle.CodeStyleManager
12+
import org.domaframework.doma.intellij.common.util.StringUtil
13+
import org.domaframework.doma.intellij.formatter.visitor.FormattingTask
14+
15+
class InjectionSqlFormatter(
16+
private val project: Project,
17+
) {
18+
companion object {
19+
private const val TEMP_FILE_PREFIX = "temp_format"
20+
private const val SQL_FILE_EXTENSION = ".sql"
21+
private const val TRIPLE_QUOTE = "\"\"\""
22+
private const val WRITE_COMMAND_NAME = "Format Injected SQL"
23+
private const val BASE_INDENT = "\t\t\t"
24+
private val COMMENT_START_REGEX = Regex("^[ \t]*/[*][ \t]*\\*")
25+
}
26+
27+
private val injectionManager by lazy { InjectedLanguageManager.getInstance(project) }
28+
private val documentManager by lazy { PsiDocumentManager.getInstance(project) }
29+
private val codeStyleManager by lazy { CodeStyleManager.getInstance(project) }
30+
private val fileTypeManager by lazy { FileTypeManager.getInstance() }
31+
private val elementFactory by lazy {
32+
com.intellij.psi.JavaPsiFacade
33+
.getElementFactory(project)
34+
}
35+
36+
fun processFormattingTask(
37+
task: FormattingTask,
38+
removeSpace: (String) -> String,
39+
) {
40+
// Apply PreProcessor to single-line injection SQL
41+
val injectionFile =
42+
injectionManager
43+
.getInjectedPsiFiles(task.expression)
44+
?.firstOrNull()
45+
?.first as? PsiFile ?: return
46+
47+
// format(task.isOriginalTextBlock, task.formattedText, injectionFile, removeSpace)
48+
val formattedText =
49+
if (!task.isOriginalTextBlock) {
50+
val result =
51+
SqlFormatPreProcessor().updateDocument(injectionFile, injectionFile.textRange)
52+
result.document?.text ?: return
53+
} else {
54+
task.formattedText
55+
}
56+
replaceHostStringLiteral(FormattingTask(task.expression, formattedText, task.isOriginalTextBlock), removeSpace)
57+
}
58+
59+
fun format(
60+
sqlFile: PsiFile,
61+
removeSpace: (String) -> String,
62+
) {
63+
val result = SqlFormatPreProcessor().updateDocument(sqlFile, sqlFile.textRange)
64+
val formattedText = result.document?.text ?: return
65+
66+
val tmpFormatted = formatAsTemporarySqlFile(formattedText)
67+
val document = documentManager.getDocument(sqlFile) ?: return
68+
document.replaceString(
69+
sqlFile.textRange.startOffset,
70+
sqlFile.textRange.endOffset,
71+
tmpFormatted,
72+
)
73+
documentManager.commitDocument(document)
74+
removeSpace(document.text)
75+
}
76+
77+
private fun removeIndentLines(sqlText: String): String =
78+
sqlText.lines().joinToString(StringUtil.LINE_SEPARATE) { line ->
79+
val processedLine =
80+
if (COMMENT_START_REGEX.containsMatchIn(line)) {
81+
// Remove spaces between /* and comment content, as IntelliJ Java formatter may insert them
82+
line.replace(COMMENT_START_REGEX, "/**")
83+
} else {
84+
line
85+
}
86+
processedLine.dropWhile { it.isWhitespace() }
87+
}
88+
89+
/**
90+
* Formats SQL text by creating a temporary SQL file and applying code style.
91+
* Returns original text if formatting fails.
92+
*/
93+
private fun formatAsTemporarySqlFile(sqlText: String): String =
94+
runCatching {
95+
val tempFileName = "${TEMP_FILE_PREFIX}${SQL_FILE_EXTENSION}"
96+
val fileType = fileTypeManager.getFileTypeByExtension("sql")
97+
val tempSqlFile =
98+
PsiFileFactory
99+
.getInstance(project)
100+
.createFileFromText(tempFileName, fileType, sqlText)
101+
102+
codeStyleManager.reformatText(tempSqlFile, 0, tempSqlFile.textLength)
103+
tempSqlFile.text
104+
}.getOrDefault(sqlText)
105+
106+
/**
107+
* Replaces the host Java string literal with formatted SQL text.
108+
*/
109+
private fun replaceHostStringLiteral(
110+
task: FormattingTask,
111+
sqlPostProcessorProcess: (String) -> String,
112+
) {
113+
runCatching {
114+
val formattedLiteral = createFormattedLiteral(task, sqlPostProcessorProcess)
115+
replaceInDocument(task.expression, formattedLiteral)
116+
}
117+
}
118+
119+
private fun createFormattedLiteral(
120+
task: FormattingTask,
121+
sqlPostProcessorProcess: (String) -> String,
122+
): String {
123+
// Format SQL to match regular SQL file formatting
124+
val sqlWithoutIndent = removeIndentLines(task.formattedText)
125+
val formattedSql = formatAsTemporarySqlFile(sqlWithoutIndent)
126+
val processedSql = sqlPostProcessorProcess(formattedSql)
127+
128+
// Create properly aligned literal text
129+
val literalText = createFormattedLiteralText(processedSql)
130+
val normalizedText = normalizeIndentation(literalText)
131+
132+
val newLiteral = elementFactory.createExpressionFromText(normalizedText, task.expression)
133+
return newLiteral.text
134+
}
135+
136+
/**
137+
* Creates a Java text block (triple-quoted string) from formatted SQL.
138+
*/
139+
private fun createFormattedLiteralText(formattedSqlText: String): String =
140+
buildString {
141+
append(TRIPLE_QUOTE)
142+
append(StringUtil.LINE_SEPARATE)
143+
append(formattedSqlText)
144+
append(TRIPLE_QUOTE)
145+
}
146+
147+
/**
148+
* Normalizes indentation by removing base indent and reapplying it consistently.
149+
*/
150+
private fun normalizeIndentation(sqlText: String): String {
151+
val lines = sqlText.lines()
152+
if (lines.isEmpty()) return sqlText
153+
154+
val (literalSeparator, contentLines) = lines.first() to lines.drop(1)
155+
val normalizedLines =
156+
contentLines.map { line ->
157+
when {
158+
line.isBlank() -> line
159+
else ->
160+
BASE_INDENT +
161+
line.removePrefix(
162+
BASE_INDENT,
163+
)
164+
}
165+
}
166+
167+
return buildString {
168+
append(literalSeparator)
169+
if (normalizedLines.isNotEmpty()) {
170+
append(StringUtil.LINE_SEPARATE)
171+
append(normalizedLines.joinToString(StringUtil.LINE_SEPARATE))
172+
}
173+
}
174+
}
175+
176+
private fun replaceInDocument(
177+
expression: PsiLiteralExpression,
178+
newText: String,
179+
) {
180+
val document = documentManager.getDocument(expression.containingFile) ?: return
181+
val range = expression.textRange
182+
183+
document.replaceString(range.startOffset, range.endOffset, newText)
184+
documentManager.commitDocument(document)
185+
}
186+
187+
fun processAllTextBlock(formattingTasks: MutableList<FormattingTask>) {
188+
if (formattingTasks.isEmpty()) return
189+
190+
WriteCommandAction.runWriteCommandAction(
191+
project,
192+
WRITE_COMMAND_NAME,
193+
null,
194+
{
195+
// Convert PsiLiteralExpression to text blocks first
196+
formattingTasks
197+
.sortedByDescending { it.expression.textRange.startOffset }
198+
.forEach { task ->
199+
convertExpressionToTextBlock(task.expression)
200+
}
201+
},
202+
)
203+
}
204+
205+
fun convertExpressionToTextBlock(expression: PsiLiteralExpression) {
206+
if (!expression.isValid || expression.isTextBlock) return
207+
208+
val oldText = expression.value?.toString() ?: return
209+
val newText = convertToTextBlock(oldText)
210+
val document = documentManager.getDocument(expression.containingFile) ?: return
211+
212+
val range = expression.textRange
213+
document.replaceString(range.startOffset, range.endOffset, newText)
214+
documentManager.commitDocument(document)
215+
}
216+
217+
private fun convertToTextBlock(content: String): String = "\"\"\"\n${content.replace("\"\"\"", "\\\"\\\"\\\"")}${TRIPLE_QUOTE}"
218+
}

src/main/kotlin/org/domaframework/doma/intellij/formatter/processor/SqlInjectionPostProcessor.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ class SqlInjectionPostProcessor : SqlPostProcessor() {
8181
val host = InjectionSqlUtil.getLiteralExpressionHost(source) ?: return
8282
val originalText = host.value?.toString() ?: return
8383

84-
val visitor = DaoInjectionSqlVisitor(source.project)
84+
val injectionFormatter = InjectionSqlFormatter(source.project)
8585
val formattingTask = FormattingTask(host, originalText, host.isTextBlock)
8686

87-
visitor.convertExpressionToTextBlock(formattingTask.expression)
88-
visitor.processFormattingTask(formattingTask) { text ->
87+
injectionFormatter.convertExpressionToTextBlock(formattingTask.expression)
88+
injectionFormatter.processFormattingTask(formattingTask) { text ->
8989
processDocumentText(text)
9090
}
9191
}

0 commit comments

Comments
 (0)