Skip to content

Commit d804142

Browse files
committed
feat(language-injector): optimize code block injection logic and add support for whitespace-sensitive language injections #101
1 parent e7a2fd1 commit d804142

File tree

2 files changed

+99
-9
lines changed

2 files changed

+99
-9
lines changed

exts/devin-lang/src/main/kotlin/cc/unitmesh/language/DevInLanguageInjector.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cc.unitmesh.language
22

33
import cc.unitmesh.language.parser.CodeBlockElement
44
import cc.unitmesh.language.psi.DevInTypes
5+
import com.intellij.lang.Language
56
import com.intellij.openapi.util.TextRange
67
import com.intellij.psi.InjectedLanguagePlaces
78
import com.intellij.psi.LanguageInjector
@@ -29,6 +30,12 @@ class DevInLanguageInjector : LanguageInjector {
2930
return
3031
}
3132

33+
injectAsOnePlace(host, language, registrar)
34+
}
35+
36+
private fun injectAsOnePlace(host: CodeBlockElement, language: Language, registrar: InjectedLanguagePlaces) {
37+
val elements = CodeBlockElement.obtainFenceContent(host, withWhitespaces = true) ?: return
38+
3239
val first = elements.first()
3340
val last = elements.last()
3441

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1+
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
12
package cc.unitmesh.language.parser
23

34
import cc.unitmesh.language.psi.DevInTypes
45
import com.intellij.extapi.psi.ASTWrapperPsiElement
56
import com.intellij.lang.ASTNode
7+
import com.intellij.openapi.util.TextRange
68
import com.intellij.psi.ElementManipulators
79
import com.intellij.psi.LiteralTextEscaper
810
import com.intellij.psi.PsiElement
911
import com.intellij.psi.PsiLanguageInjectionHost
1012
import com.intellij.psi.impl.source.tree.injected.InjectionBackgroundSuppressor
11-
import com.intellij.psi.util.PsiTreeUtil
12-
import com.intellij.psi.util.elementType
13+
import com.intellij.psi.tree.IElementType
14+
import com.intellij.psi.util.*
1315

1416
class CodeBlockElement(node: ASTNode) : ASTWrapperPsiElement(node), PsiLanguageInjectionHost,
1517
InjectionBackgroundSuppressor {
1618
override fun isValidHost(): Boolean {
19+
// use MarkdownCodeFenceUtils.isAbleToAcceptInjections
1720
return true
1821
}
1922

@@ -22,20 +25,100 @@ class CodeBlockElement(node: ASTNode) : ASTWrapperPsiElement(node), PsiLanguageI
2225
}
2326

2427
override fun createLiteralTextEscaper(): LiteralTextEscaper<out PsiLanguageInjectionHost> {
25-
return LiteralTextEscaper.createSimple(this)
28+
return CodeBlockLiteralTextEscaper(this)
2629
}
2730

2831
fun getLanguageId(): PsiElement? {
2932
return findChildByType(DevInTypes.LANGUAGE_ID)
3033
}
3134

3235
fun getContents(): List<PsiElement> {
33-
val codeContents = children.filter { it.elementType == DevInTypes.CODE_CONTENTS }.firstOrNull() ?: return emptyList()
36+
return obtainFenceContent(this, withWhitespaces = false) ?: return emptyList()
37+
}
38+
39+
companion object {
40+
fun obtainFenceContent(element: CodeBlockElement, withWhitespaces: Boolean): List<PsiElement>? {
41+
return when {
42+
withWhitespaces -> CachedValuesManager.getCachedValue(element) {
43+
CachedValueProvider.Result.create(getContent(element, true), element)
44+
}
45+
else -> CachedValuesManager.getCachedValue(element) {
46+
CachedValueProvider.Result.create(getContent(element, false), element)
47+
}
48+
}
49+
}
50+
51+
private fun getContent(element: CodeBlockElement, withWhitespaces: Boolean): List<PsiElement>? {
52+
val codeContents = element.children.firstOrNull { it.elementType == DevInTypes.CODE_CONTENTS } ?: return null
53+
54+
val psiElements = PsiTreeUtil.collectElements(codeContents) {
55+
it.elementType == DevInTypes.CODE_CONTENT
56+
}.toMutableList()
57+
58+
return psiElements.toList()
59+
}
60+
61+
fun obtainRelevantTextRange(element: CodeBlockElement): TextRange {
62+
val elements = obtainFenceContent(element, withWhitespaces = true) ?: return getEmptyRange(element)
63+
val first = elements.first()
64+
val last = elements.last()
65+
return TextRange.create(first.startOffsetInParent, last.startOffsetInParent + last.textLength)
66+
}
67+
68+
private fun getEmptyRange(host: CodeBlockElement): TextRange {
69+
val start = host.children.find { it.hasType(DevInTypes.LANGUAGE_ID) }
70+
?: host.children.find { it.hasType(DevInTypes.CODE_BLOCK_START) }
71+
72+
return TextRange.from(start!!.startOffsetInParent + start.textLength + 1, 0)
73+
}
74+
}
75+
}
76+
77+
fun PsiElement.hasType(type: IElementType): Boolean {
78+
return PsiUtilCore.getElementType(this) == type
79+
}
80+
81+
class CodeBlockLiteralTextEscaper(host: CodeBlockElement) : LiteralTextEscaper<CodeBlockElement>(host) {
82+
override fun isOneLine(): Boolean = false;
83+
84+
override fun decode(rangeInsideHost: TextRange, outChars: StringBuilder): Boolean {
85+
val elements = CodeBlockElement.obtainFenceContent(myHost, withWhitespaces = false) ?: return true
86+
for (element in elements) {
87+
val intersected = rangeInsideHost.intersection(element.textRangeInParent) ?: continue
88+
outChars.append(intersected.substring(myHost.text))
89+
}
90+
return true
91+
}
3492

35-
val psiElements = PsiTreeUtil.collectElements(codeContents) {
36-
it.elementType == DevInTypes.CODE_CONTENT
37-
}.toMutableList()
93+
override fun getOffsetInHost(offsetInDecoded: Int, rangeInsideHost: TextRange): Int {
94+
val elements = CodeBlockElement.obtainFenceContent(myHost, withWhitespaces = false) ?: return -1
95+
var cur = 0
96+
for (element in elements) {
97+
val intersected = rangeInsideHost.intersection(element.textRangeInParent)
98+
if (intersected == null || intersected.isEmpty) continue
99+
if (cur + intersected.length == offsetInDecoded) {
100+
return intersected.startOffset + intersected.length
101+
}
102+
else if (cur == offsetInDecoded) {
103+
return intersected.startOffset
104+
}
105+
else if (cur < offsetInDecoded && cur + intersected.length > offsetInDecoded) {
106+
return intersected.startOffset + (offsetInDecoded - cur)
107+
}
108+
cur += intersected.length
109+
}
110+
111+
val last = elements[elements.size - 1]
112+
val intersected = rangeInsideHost.intersection(last.textRangeInParent)
113+
if (intersected == null || intersected.isEmpty) return -1
114+
val result = intersected.startOffset + (offsetInDecoded - (cur - intersected.length))
115+
return if (rangeInsideHost.startOffset <= result && result <= rangeInsideHost.endOffset) {
116+
result
117+
}
118+
else -1
119+
}
38120

39-
return psiElements.toList()
121+
override fun getRelevantTextRange(): TextRange {
122+
return CodeBlockElement.obtainRelevantTextRange(myHost)
40123
}
41-
}
124+
}

0 commit comments

Comments
 (0)