diff --git a/src/main/kotlin/com/github/tempest/framework/views/completion/TemplateBracketBackspaceHandler.kt b/src/main/kotlin/com/github/tempest/framework/views/completion/TemplateBracketBackspaceHandler.kt
new file mode 100644
index 0000000..0c0b591
--- /dev/null
+++ b/src/main/kotlin/com/github/tempest/framework/views/completion/TemplateBracketBackspaceHandler.kt
@@ -0,0 +1,24 @@
+package com.github.tempest.framework.views.completion
+
+import com.github.tempest.framework.TempestFrameworkUtil
+import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate
+import com.intellij.openapi.editor.Editor
+import com.intellij.psi.PsiFile
+
+class TemplateBracketBackspaceHandler : BackspaceHandlerDelegate() {
+
+ override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) = Unit
+
+ override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
+ if (!file.name.endsWith(TempestFrameworkUtil.TEMPLATE_SUFFIX) || c !in SPECIAL_CHARS) {
+ return false
+ }
+
+ TemplateBracketTypedHandler.INSTANCE.synchronizeBracketsAfterDeletion(file.project, editor, c)
+ return false
+ }
+
+ private companion object {
+ val SPECIAL_CHARS = setOf('!', '-')
+ }
+}
diff --git a/src/main/kotlin/com/github/tempest/framework/views/completion/TemplateBracketTypedHandler.kt b/src/main/kotlin/com/github/tempest/framework/views/completion/TemplateBracketTypedHandler.kt
new file mode 100644
index 0000000..556aab5
--- /dev/null
+++ b/src/main/kotlin/com/github/tempest/framework/views/completion/TemplateBracketTypedHandler.kt
@@ -0,0 +1,138 @@
+package com.github.tempest.framework.views.completion
+
+import com.github.tempest.framework.TempestFrameworkUtil
+import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiFile
+
+class TemplateBracketTypedHandler : TypedHandlerDelegate() {
+
+ data class BracketPair(val opening: String, val closing: String)
+
+ override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
+ if (!file.name.endsWith(TempestFrameworkUtil.TEMPLATE_SUFFIX)) return Result.CONTINUE
+
+ val offset = editor.caretModel.offset
+ val text = editor.document.charsSequence
+
+ return when (c) {
+ '{' -> {
+ insertClosingBracket(project, editor, offset, "}")
+ Result.STOP
+ }
+ '!', '-' -> {
+ if (text.matchesAt(offset - 3, DOUBLE_BRACE_OPEN)) {
+ handleDoubledSpecialChar(project, editor, offset, c)
+ Result.STOP
+ } else {
+ Result.CONTINUE
+ }
+ }
+ ' ' -> {
+ handleSpaceInBrackets(project, editor, text, offset)
+ Result.CONTINUE
+ }
+ else -> Result.CONTINUE
+ }
+ }
+
+ private fun handleDoubledSpecialChar(project: Project, editor: Editor, offset: Int, char: Char) {
+ WriteCommandAction.runWriteCommandAction(project) {
+ editor.document.insertString(offset, char.toString())
+ editor.caretModel.moveToOffset(offset + 1)
+ transformClosingBracket(editor, offset + 1, char)
+ }
+ }
+
+ private fun handleSpaceInBrackets(project: Project, editor: Editor, text: CharSequence, offset: Int) {
+ BRACKET_PAIRS.firstOrNull { pair ->
+ text.matchesAt(offset - pair.opening.length - 1, pair.opening) &&
+ text[offset - 1] == ' ' &&
+ text.matchesAt(offset, pair.closing)
+ }?.let {
+ WriteCommandAction.runWriteCommandAction(project) {
+ editor.document.insertString(offset, " ")
+ }
+ }
+ }
+
+ private fun transformClosingBracket(editor: Editor, offset: Int, char: Char) {
+ val text = editor.document.charsSequence
+ val newClosing = "$char$char$DOUBLE_BRACE_CLOSE"
+
+ findNextUnnestedClosing(text, offset, DOUBLE_BRACE_CLOSE)?.let { closingIndex ->
+ editor.document.replaceString(closingIndex, closingIndex + 2, newClosing)
+ }
+ }
+
+ fun synchronizeBracketsAfterDeletion(project: Project, editor: Editor, deletedChar: Char) {
+ val offset = editor.caretModel.offset
+ val text = editor.document.charsSequence
+
+ WriteCommandAction.runWriteCommandAction(project) {
+ when {
+ text.matchesAt(offset - 3, DOUBLE_BRACE_OPEN) && text.getOrNull(offset - 1) == deletedChar -> {
+ editor.document.deleteString(offset - 1, offset)
+ transformClosingAfterDeletion(editor, offset - 1, deletedChar)
+ }
+ text.matchesAt(offset - 2, DOUBLE_BRACE_OPEN) && text.getOrNull(offset) == deletedChar -> {
+ editor.document.deleteString(offset, offset + 1)
+ transformClosingAfterDeletion(editor, offset, deletedChar)
+ }
+ }
+ }
+ }
+
+ private fun transformClosingAfterDeletion(editor: Editor, offset: Int, char: Char) {
+ val text = editor.document.charsSequence
+ val doubledClosing = "$char$char$DOUBLE_BRACE_CLOSE"
+
+ findNextUnnestedClosing(text, offset, doubledClosing)?.let { closingIndex ->
+ editor.document.replaceString(closingIndex, closingIndex + 4, DOUBLE_BRACE_CLOSE)
+ }
+ }
+
+ private fun insertClosingBracket(project: Project, editor: Editor, offset: Int, closing: String) {
+ WriteCommandAction.runWriteCommandAction(project) {
+ editor.document.insertString(offset, closing)
+ }
+ }
+
+ private fun findNextUnnestedClosing(text: CharSequence, startOffset: Int, pattern: String): Int? {
+ val patternLength = pattern.length
+ val searchRange = startOffset..(text.length - patternLength)
+
+ for (i in searchRange) {
+ if (text.matchesAt(i, pattern) && !hasOpeningBetween(text, startOffset, i)) {
+ return i
+ }
+ }
+ return null
+ }
+
+ private fun hasOpeningBetween(text: CharSequence, start: Int, end: Int): Boolean {
+ return (start until end - 1).any { i ->
+ text.matchesAt(i, DOUBLE_BRACE_OPEN)
+ }
+ }
+
+ companion object {
+ val INSTANCE = TemplateBracketTypedHandler()
+
+ private const val DOUBLE_BRACE_OPEN = "{{"
+ private const val DOUBLE_BRACE_CLOSE = "}}"
+ }
+}
+
+private fun CharSequence.matchesAt(index: Int, pattern: String): Boolean {
+ if (index < 0 || index + pattern.length > length) return false
+ return (pattern.indices).all { i -> this[index + i] == pattern[i] }
+}
+
+val BRACKET_PAIRS = listOf(
+ TemplateBracketTypedHandler.BracketPair("{{--", "--}}"),
+ TemplateBracketTypedHandler.BracketPair("{{!!", "!!}}"),
+ TemplateBracketTypedHandler.BracketPair("{{", "}}"),
+)
\ No newline at end of file
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 31f570b..442b6ee 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -72,6 +72,10 @@
implementation="com.github.tempest.framework.db.references.QueryBuilderReferenceContributor"/>
+
+