Skip to content

Commit ea54924

Browse files
committed
Create view intention and live template
1 parent 469f12c commit ea54924

File tree

5 files changed

+189
-0
lines changed

5 files changed

+189
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package app.cash.sqldelight.intellij.intentions
2+
3+
import com.alecstrong.sql.psi.core.psi.SqlCompoundSelectStmt
4+
import com.alecstrong.sql.psi.core.psi.SqlCreateViewStmt
5+
import com.alecstrong.sql.psi.core.psi.SqlStmtList
6+
import com.alecstrong.sql.psi.core.psi.SqlTypes
7+
import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction
8+
import com.intellij.codeInsight.template.TemplateManager
9+
import com.intellij.codeInsight.template.impl.TextExpression
10+
import com.intellij.openapi.command.WriteCommandAction
11+
import com.intellij.openapi.editor.Document
12+
import com.intellij.openapi.editor.Editor
13+
import com.intellij.openapi.project.Project
14+
import com.intellij.psi.PsiDocumentManager
15+
import com.intellij.psi.PsiElement
16+
import com.intellij.psi.util.PsiTreeUtil
17+
import com.intellij.psi.util.parentOfType
18+
import org.jetbrains.kotlin.psi.psiUtil.endOffset
19+
import org.jetbrains.kotlin.psi.psiUtil.startOffset
20+
21+
internal class CreateViewIntention : BaseElementAtCaretIntentionAction() {
22+
23+
override fun getFamilyName(): String {
24+
return INTENTIONS_FAMILY_NAME_REFACTORINGS
25+
}
26+
27+
override fun getText(): String {
28+
return "Create view"
29+
}
30+
31+
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
32+
val selectStmt = element.parentOfType<SqlCompoundSelectStmt>(true)
33+
return selectStmt != null && selectStmt.parentOfType<SqlCreateViewStmt>() == null
34+
}
35+
36+
override fun invoke(project: Project, editor: Editor, element: PsiElement) {
37+
PsiDocumentManager.getInstance(project).commitAllDocuments()
38+
39+
val selectStmt = element.parentOfType<SqlCompoundSelectStmt>(true) ?: return
40+
val stmtList = selectStmt.parentOfType<SqlStmtList>() ?: return
41+
val container = stmtList.stmtList.firstOrNull { PsiTreeUtil.isAncestor(it, selectStmt, true) } ?: return
42+
43+
val semi = PsiTreeUtil.findSiblingForward(container, SqlTypes.SEMI, false, null) ?: return
44+
45+
val containerStart = container.startOffset
46+
val containerEnd = semi.endOffset
47+
val text = editor.document.getDocumentTextFragment(containerStart, containerEnd)
48+
val offset = selectStmt.startOffset - containerStart
49+
50+
val templateManager = TemplateManager.getInstance(project)
51+
52+
WriteCommandAction.runWriteCommandAction(project) {
53+
editor.document.deleteString(containerStart, containerEnd)
54+
55+
val template = templateManager.createTemplate("", "")
56+
template.addTextSegment(text.substring(0, offset))
57+
template.addTextSegment("SELECT * FROM ")
58+
val expression = TextExpression("some_view")
59+
template.addVariableSegment("NAME")
60+
template.addSelectionStartVariable()
61+
template.addTextSegment(text.substring(selectStmt.endOffset - containerStart))
62+
63+
template.addTextSegment("\n\nCREATE VIEW ")
64+
template.addVariable("NAME", expression, true)
65+
template.addTextSegment(" AS ${selectStmt.text};")
66+
67+
templateManager.startTemplate(editor, template)
68+
}
69+
}
70+
71+
private fun Document.getDocumentTextFragment(startOffset: Int, endOffset: Int): String {
72+
return charsSequence.subSequence(startOffset, endOffset).toString()
73+
}
74+
}

sqldelight-idea-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@
136136
<className>app.cash.sqldelight.intellij.intentions.QualifyColumnNameIntention</className>
137137
<category>SQLDelight</category>
138138
</intentionAction>
139+
<intentionAction>
140+
<className>app.cash.sqldelight.intellij.intentions.CreateViewIntention</className>
141+
<category>SQLDelight</category>
142+
</intentionAction>
139143

140144
<copyPastePostProcessor
141145
implementation="app.cash.sqldelight.intellij.SqlDelightCopyPasteProcessor"/>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
Creates a VIEW from SELECT statement
4+
</body>
5+
</html>

sqldelight-idea-plugin/src/main/resources/liveTemplates/SqlDelight.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,12 @@
6868
<option name="SQLDELIGHT" value="true" />
6969
</context>
7070
</template>
71+
<template name="view" value="CREATE VIEW $view$ AS SELECT * FROM $table$$END$;"
72+
description="new view definition" toReformat="false" toShortenFQNames="true">
73+
<variable name="view" expression="" defaultValue="" alwaysStopAt="true"/>
74+
<variable name="table" expression="complete()" defaultValue="" alwaysStopAt="true"/>
75+
<context>
76+
<option name="SQLDELIGHT" value="true"/>
77+
</context>
78+
</template>
7179
</templateSet>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package app.cash.sqldelight.intellij.intentions
2+
3+
import app.cash.sqldelight.core.lang.SqlDelightFileType
4+
import app.cash.sqldelight.intellij.SqlDelightFixtureTestCase
5+
import com.google.common.truth.Truth.assertThat
6+
7+
class CreateViewIntentionTest : SqlDelightFixtureTestCase() {
8+
9+
fun testIntentionAvailableOnSelectStmt() {
10+
myFixture.configureByText(
11+
SqlDelightFileType,
12+
CREATE_TABLE + """
13+
|SELECT column_1
14+
|FROM ta<caret>ble_1;
15+
""".trimMargin(),
16+
)
17+
18+
val intention = CreateViewIntention()
19+
20+
assertThat(
21+
intention.isAvailable(
22+
myFixture.project,
23+
myFixture.editor,
24+
myFixture.file.findElementAt(myFixture.editor.caretModel.offset)!!,
25+
),
26+
)
27+
.isTrue()
28+
}
29+
30+
fun testIntentionNotAvailableInsideCreateView() {
31+
myFixture.configureByText(
32+
SqlDelightFileType,
33+
CREATE_TABLE + """
34+
|SELECT * FROM some_view;
35+
|
36+
|CREATE VIEW some_view AS SE<caret>LECT * FROM table_1;
37+
""".trimMargin(),
38+
)
39+
40+
val intention = CreateViewIntention()
41+
42+
assertThat(
43+
intention.isAvailable(
44+
myFixture.project,
45+
myFixture.editor,
46+
myFixture.file.findElementAt(myFixture.editor.caretModel.offset)!!,
47+
),
48+
)
49+
.isFalse()
50+
}
51+
52+
fun testIntentionExecution() {
53+
myFixture.configureByText(
54+
SqlDelightFileType,
55+
CREATE_TABLE + """
56+
|SELECT column_1
57+
|FROM table_1
58+
|WHERE column_1 = (
59+
| SELECT co<caret>lumn_2
60+
| FROM table_2
61+
|);
62+
""".trimMargin(),
63+
)
64+
65+
val intention = CreateViewIntention()
66+
intention.invoke(
67+
myFixture.project,
68+
myFixture.editor,
69+
myFixture.file.findElementAt(myFixture.editor.caretModel.offset)!!,
70+
)
71+
72+
myFixture.checkResult(
73+
CREATE_TABLE + """
74+
|SELECT column_1
75+
|FROM table_1
76+
|WHERE column_1 = (
77+
| SELECT * FROM some_view
78+
|);
79+
|
80+
|CREATE VIEW some_view AS SELECT column_2
81+
| FROM table_2;
82+
""".trimMargin(),
83+
)
84+
}
85+
86+
companion object {
87+
val CREATE_TABLE = """
88+
|CREATE TABLE table_1 (
89+
| column_1 INTEGER NOT NULL
90+
|);
91+
|
92+
|CREATE TABLE table_2 (
93+
| column_2 INTEGER NOT NULL
94+
|);
95+
|
96+
""".trimMargin()
97+
}
98+
}

0 commit comments

Comments
 (0)