Skip to content

Commit 9e7ada6

Browse files
authored
feat: autocomplete brackets (#46)
2 parents f6ea236 + 9577041 commit 9e7ada6

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.tempest.framework.views.completion
2+
3+
import com.github.tempest.framework.TempestFrameworkUtil
4+
import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate
5+
import com.intellij.openapi.editor.Editor
6+
import com.intellij.psi.PsiFile
7+
8+
class TemplateBracketBackspaceHandler : BackspaceHandlerDelegate() {
9+
10+
override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) = Unit
11+
12+
override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
13+
if (!file.name.endsWith(TempestFrameworkUtil.TEMPLATE_SUFFIX) || c !in SPECIAL_CHARS) {
14+
return false
15+
}
16+
17+
TemplateBracketTypedHandler.INSTANCE.synchronizeBracketsAfterDeletion(file.project, editor, c)
18+
return false
19+
}
20+
21+
private companion object {
22+
val SPECIAL_CHARS = setOf('!', '-')
23+
}
24+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.github.tempest.framework.views.completion
2+
3+
import com.github.tempest.framework.TempestFrameworkUtil
4+
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
5+
import com.intellij.openapi.command.WriteCommandAction
6+
import com.intellij.openapi.editor.Editor
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.psi.PsiFile
9+
10+
class TemplateBracketTypedHandler : TypedHandlerDelegate() {
11+
12+
data class BracketPair(val opening: String, val closing: String)
13+
14+
override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
15+
if (!file.name.endsWith(TempestFrameworkUtil.TEMPLATE_SUFFIX)) return Result.CONTINUE
16+
17+
val offset = editor.caretModel.offset
18+
val text = editor.document.charsSequence
19+
20+
return when (c) {
21+
'{' -> {
22+
insertClosingBracket(project, editor, offset, "}")
23+
Result.STOP
24+
}
25+
'!', '-' -> {
26+
if (text.matchesAt(offset - 3, DOUBLE_BRACE_OPEN)) {
27+
handleDoubledSpecialChar(project, editor, offset, c)
28+
Result.STOP
29+
} else {
30+
Result.CONTINUE
31+
}
32+
}
33+
' ' -> {
34+
handleSpaceInBrackets(project, editor, text, offset)
35+
Result.CONTINUE
36+
}
37+
else -> Result.CONTINUE
38+
}
39+
}
40+
41+
private fun handleDoubledSpecialChar(project: Project, editor: Editor, offset: Int, char: Char) {
42+
WriteCommandAction.runWriteCommandAction(project) {
43+
editor.document.insertString(offset, char.toString())
44+
editor.caretModel.moveToOffset(offset + 1)
45+
transformClosingBracket(editor, offset + 1, char)
46+
}
47+
}
48+
49+
private fun handleSpaceInBrackets(project: Project, editor: Editor, text: CharSequence, offset: Int) {
50+
BRACKET_PAIRS.firstOrNull { pair ->
51+
text.matchesAt(offset - pair.opening.length - 1, pair.opening) &&
52+
text[offset - 1] == ' ' &&
53+
text.matchesAt(offset, pair.closing)
54+
}?.let {
55+
WriteCommandAction.runWriteCommandAction(project) {
56+
editor.document.insertString(offset, " ")
57+
}
58+
}
59+
}
60+
61+
private fun transformClosingBracket(editor: Editor, offset: Int, char: Char) {
62+
val text = editor.document.charsSequence
63+
val newClosing = "$char$char$DOUBLE_BRACE_CLOSE"
64+
65+
findNextUnnestedClosing(text, offset, DOUBLE_BRACE_CLOSE)?.let { closingIndex ->
66+
editor.document.replaceString(closingIndex, closingIndex + 2, newClosing)
67+
}
68+
}
69+
70+
fun synchronizeBracketsAfterDeletion(project: Project, editor: Editor, deletedChar: Char) {
71+
val offset = editor.caretModel.offset
72+
val text = editor.document.charsSequence
73+
74+
WriteCommandAction.runWriteCommandAction(project) {
75+
when {
76+
text.matchesAt(offset - 3, DOUBLE_BRACE_OPEN) && text.getOrNull(offset - 1) == deletedChar -> {
77+
editor.document.deleteString(offset - 1, offset)
78+
transformClosingAfterDeletion(editor, offset - 1, deletedChar)
79+
}
80+
text.matchesAt(offset - 2, DOUBLE_BRACE_OPEN) && text.getOrNull(offset) == deletedChar -> {
81+
editor.document.deleteString(offset, offset + 1)
82+
transformClosingAfterDeletion(editor, offset, deletedChar)
83+
}
84+
}
85+
}
86+
}
87+
88+
private fun transformClosingAfterDeletion(editor: Editor, offset: Int, char: Char) {
89+
val text = editor.document.charsSequence
90+
val doubledClosing = "$char$char$DOUBLE_BRACE_CLOSE"
91+
92+
findNextUnnestedClosing(text, offset, doubledClosing)?.let { closingIndex ->
93+
editor.document.replaceString(closingIndex, closingIndex + 4, DOUBLE_BRACE_CLOSE)
94+
}
95+
}
96+
97+
private fun insertClosingBracket(project: Project, editor: Editor, offset: Int, closing: String) {
98+
WriteCommandAction.runWriteCommandAction(project) {
99+
editor.document.insertString(offset, closing)
100+
}
101+
}
102+
103+
private fun findNextUnnestedClosing(text: CharSequence, startOffset: Int, pattern: String): Int? {
104+
val patternLength = pattern.length
105+
val searchRange = startOffset..(text.length - patternLength)
106+
107+
for (i in searchRange) {
108+
if (text.matchesAt(i, pattern) && !hasOpeningBetween(text, startOffset, i)) {
109+
return i
110+
}
111+
}
112+
return null
113+
}
114+
115+
private fun hasOpeningBetween(text: CharSequence, start: Int, end: Int): Boolean {
116+
return (start until end - 1).any { i ->
117+
text.matchesAt(i, DOUBLE_BRACE_OPEN)
118+
}
119+
}
120+
121+
companion object {
122+
val INSTANCE = TemplateBracketTypedHandler()
123+
124+
private const val DOUBLE_BRACE_OPEN = "{{"
125+
private const val DOUBLE_BRACE_CLOSE = "}}"
126+
}
127+
}
128+
129+
private fun CharSequence.matchesAt(index: Int, pattern: String): Boolean {
130+
if (index < 0 || index + pattern.length > length) return false
131+
return (pattern.indices).all { i -> this[index + i] == pattern[i] }
132+
}
133+
134+
val BRACKET_PAIRS = listOf(
135+
TemplateBracketTypedHandler.BracketPair("{{--", "--}}"),
136+
TemplateBracketTypedHandler.BracketPair("{{!!", "!!}}"),
137+
TemplateBracketTypedHandler.BracketPair("{{", "}}"),
138+
)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
implementation="com.github.tempest.framework.db.references.QueryBuilderReferenceContributor"/>
7373
<typedHandler
7474
implementation="com.github.tempest.framework.views.completion.ViewVariableTypedHandler"/>
75+
<typedHandler
76+
implementation="com.github.tempest.framework.views.completion.TemplateBracketTypedHandler"/>
77+
<backspaceHandlerDelegate
78+
implementation="com.github.tempest.framework.views.completion.TemplateBracketBackspaceHandler"/>
7579
</extensions>
7680
<extensions defaultExtensionNs="com.jetbrains.php">
7781
<libraryRoot id="tempest.meta-storm" path="meta-storm" runtime="false" />

0 commit comments

Comments
 (0)