Skip to content

Commit

Permalink
Improved argument injections handling
Browse files Browse the repository at this point in the history
  • Loading branch information
vepanimas committed Sep 5, 2020
1 parent fbfd2d4 commit 7d3f4a1
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 36 deletions.
72 changes: 59 additions & 13 deletions src/main/kotlin/com/intellij/StyledComponents/InjectionUtils.kt
Original file line number Diff line number Diff line change
@@ -1,40 +1,86 @@
package com.intellij.styledComponents

import com.intellij.lang.javascript.JSTokenTypes
import com.intellij.lang.javascript.psi.JSExpression
import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiLanguageInjectionHost
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
import com.intellij.util.ArrayUtil
import com.intellij.util.containers.ContainerUtil
import java.util.*
import kotlin.math.max

private const val EXTERNAL_FRAGMENT = "EXTERNAL_FRAGMENT"
private const val EXPERIMENTAL_INJECTIONS = "styled.components.experimental.injections"

fun getInjectionPlaces(quotedLiteral: PsiElement): List<StringPlace> {
if (quotedLiteral is JSStringTemplateExpression) {
val ranges = quotedLiteral.stringRanges
val arguments = quotedLiteral.arguments

fun getInjectionPlaces(myQuotedLiteral: PsiElement): List<StringPlace> {
if (myQuotedLiteral is JSStringTemplateExpression) {
val templateExpression = myQuotedLiteral
val ranges = templateExpression.stringRanges
if (ranges.isNotEmpty()) {
val result = ArrayList<StringPlace>(ranges.size)
val quotedLiteralNode = myQuotedLiteral.getNode()
val backquote = quotedLiteralNode.findChildByType(JSTokenTypes.BACKQUOTE)
val backquoteOffset = if (backquote != null) backquote.startOffset - quotedLiteralNode.startOffset else -1
var lastArgumentIndex = -1

for (i in ranges.indices) {
val range = ranges[i]
val prefix = if (i == 0 && range.startOffset > backquoteOffset + 1) EXTERNAL_FRAGMENT else null
val suffix = if (i < ranges.size - 1 || range.endOffset < myQuotedLiteral.getTextLength() - 1) EXTERNAL_FRAGMENT else null

// styled.div`padding: ${'none'}${'IGNORED_ARGUMENT'}${'display'}: none;`
// |_________|_______|____________________|___________|______|
// Text Suffix Ignored Prefix Text
// |__________________| |__________________|
// 1 fragment 2 fragment

var currentIndex = adjustPrecedingArgumentIndex(lastArgumentIndex, range, arguments)
val prefix = if (currentIndex != lastArgumentIndex)
getArgumentPlaceholder(arguments.elementAtOrNull(currentIndex)) else null

val suffix = getArgumentPlaceholder(arguments.elementAtOrNull(++currentIndex))

lastArgumentIndex = currentIndex
result.add(StringPlace(prefix, range, suffix))
}
return result
}
if (!ArrayUtil.isEmpty(templateExpression.arguments)) {
if (!ArrayUtil.isEmpty(arguments)) {
return ContainerUtil.emptyList()
}
}
val endOffset = Math.max(myQuotedLiteral.textLength - 1, 1)

val endOffset = max(quotedLiteral.textLength - 1, 1)
return listOf(StringPlace(null, TextRange.create(1, endOffset), null))
}


private fun adjustPrecedingArgumentIndex(startIndex: Int, range: TextRange, arguments: Array<JSExpression>): Int {
var current = startIndex

while (true) {
val argument = arguments.getOrNull(current + 1)
if (argument == null || argument.textRangeInParent.startOffset > range.endOffset) return current
current++
}
}

private fun getArgumentPlaceholder(argumentExpression: JSExpression?): String? {
if (argumentExpression == null) return null
else if (!Registry.`is`(EXPERIMENTAL_INJECTIONS)) return EXTERNAL_FRAGMENT

var hasChildInjection = false
argumentExpression.accept(object : PsiRecursiveElementWalkingVisitor() {
override fun visitElement(element: PsiElement?) {
if (element is PsiLanguageInjectionHost && StyledComponentsInjector.matchInjectionTarget(element) != null) {
hasChildInjection = true
stopWalking()
}

super.visitElement(element)
}
})

return if (hasChildInjection) null else EXTERNAL_FRAGMENT
}

data class StringPlace(val prefix: String?, val range: TextRange, val suffix: String?)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.intellij.lang.javascript.JavascriptLanguage
import com.intellij.lang.javascript.refactoring.JSNamesValidation
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileTypes.PlainTextLanguage
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComponentValidator
Expand All @@ -30,7 +30,7 @@ class StyledComponentsConfigurable(private val project: Project) : SearchableCon
private val tagsModel = TagsModel()
private val disposable = Disposer.newDisposable()
private val myTagPrefixesField = object : JBListTable(JBTable(tagsModel), disposable) {
override fun getRowRenderer(p0: Int): JBTableRowRenderer = object : EditorTextFieldJBTableRowRenderer(project, PlainTextLanguage.INSTANCE, disposable) {
override fun getRowRenderer(p0: Int): JBTableRowRenderer = object : EditorTextFieldJBTableRowRenderer(project, PlainTextFileType.INSTANCE, disposable) {
override fun getText(p0: JTable?, index: Int): String = tagsModel.myTags[index]
}

Expand All @@ -49,6 +49,7 @@ class StyledComponentsConfigurable(private val project: Project) : SearchableCon
add(editor, BorderLayout.NORTH)
validator.updateInfo(getErrorText(editor.text)?.let { ValidationInfo(it, editor) })
}

override fun getFocusableComponents(): Array<JComponent> = arrayOf(preferredFocusedComponent)
override fun getPreferredFocusedComponent(): JComponent = getComponent(0) as JComponent

Expand All @@ -72,6 +73,7 @@ class StyledComponentsConfigurable(private val project: Project) : SearchableCon
}
}
}

override fun isModified(): Boolean {
return !getPrefixesFromUi().contentEquals((myConfiguration.getTagPrefixes()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class StyledComponentsInjector : MultiHostInjector {
PlaceInfo(taggedTemplate("keyframes"), "@keyframes foo {", "}"),
PlaceInfo(jsxBodyText("style", "jsx"))
)

fun matchInjectionTarget(injectionHost: PsiLanguageInjectionHost): PlaceInfo? {
val customInjections = CustomInjectionsConfiguration.instance(injectionHost.project)
return builtinPlaces.find { (elementPattern) -> elementPattern.accepts(injectionHost) }
?: customInjections.getInjectionPlaces().find { (elementPattern) -> elementPattern.accepts(injectionHost) }
}
}

override fun elementsToInjectIn(): MutableList<out Class<out PsiElement>> {
Expand All @@ -39,9 +45,8 @@ class StyledComponentsInjector : MultiHostInjector {
override fun getLanguagesToInject(registrar: MultiHostRegistrar, injectionHost: PsiElement) {
if (injectionHost !is PsiLanguageInjectionHost)
return
val customInjections = CustomInjectionsConfiguration.instance(injectionHost.project)
val acceptedPattern = builtinPlaces.find { (elementPattern) -> elementPattern.accepts(injectionHost) }
?: customInjections.getInjectionPlaces().find { (elementPattern) -> elementPattern.accepts(injectionHost) }

val acceptedPattern = matchInjectionTarget(injectionHost)
if (acceptedPattern != null) {
val stringPlaces = getInjectionPlaces(injectionHost)
if (stringPlaces.isEmpty())
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@
id="styled-components"
displayName="styled-components"/>
<xml.attributeDescriptorsProvider implementation="com.intellij.styledComponents.CssPropAttributeDescriptorProvider"/>
<registryKey key="styled.components.experimental.injections" defaultValue="true"
description="When enabled, tries to handle nested injections"/>
</extensions>
</idea-plugin>
149 changes: 131 additions & 18 deletions src/test/InjectionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.PsiLanguageInjectionHost
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.styledComponents.CustomInjectionsConfiguration
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.containers.ContainerUtil
import org.junit.Assert

class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
class InjectionTest : BasePlatformTestCase() {

fun testTemplateArgumentIsWholeRange() {
doTest("let css = css`\${someVariable}`")
doTest("let globalCss = injectGlobal`\${someVariable}`")
Expand Down Expand Up @@ -43,12 +43,12 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
" color: \${props => props.primary ? 'white' : 'palevioletred'};\n" +
" " +
" font-size: 1em;\n" +
"`;","div {\n" +
" /* Adapt the colours based on primary prop */\n" +
" background: EXTERNAL_FRAGMENT;\n" +
" color: EXTERNAL_FRAGMENT;\n" +
" font-size: 1em;\n" +
"}")
"`;", "div {\n" +
" /* Adapt the colours based on primary prop */\n" +
" background: EXTERNAL_FRAGMENT;\n" +
" color: EXTERNAL_FRAGMENT;\n" +
" font-size: 1em;\n" +
"}")
}

fun testComplexExpression() {
Expand All @@ -64,7 +64,7 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
" color: palevioletred;\n" +
"}")
}

fun testComplexExpression2() {
doTest("const ContactMenuIcon = ((styled(Icon)))" +
".attrs({ iconName: 'contact_card' })`\n" +
Expand Down Expand Up @@ -138,26 +138,26 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
setCustomInjectionsConfiguration("media")
doTest("const Container = styled.div`\n" +
" color: #333;\n" +
" \${media.desktop `padding: 0 20px;` };\n" +
" \${media.desktop `padding: 0 20px;` }\n" +
"`", "div {\n" +
" color: #333;\n" +
" EXTERNAL_FRAGMENT;\n" +
" \n" +
"}", "div {padding: 0 20px;}")
}

fun testWithUnqualifiedCustomTag() {
setCustomInjectionsConfiguration("sc")
doTest("const Container = sc`color: #333;`;", "div {color: #333;}")
}

fun testCustomInjectionWithComplexTag() {
setCustomInjectionsConfiguration("bp")
doTest("const Container = styled.div`\n" +
" color: #333;\n" +
" \${bp(media.tablet)`padding: 0 20px;` }\n" +
"`", "div {\n" +
" color: #333;\n" +
" EXTERNAL_FRAGMENT\n" +
" \n" +
"}", "div {padding: 0 20px;}")
}

Expand All @@ -180,7 +180,7 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
fun testNoCssPropertyInjectionInHtml() {
doTestWithExtension("<div css='color:red'/>", "html", emptyArray())
}

fun testNoInjectionWithObjectInCssProperty() {
doTest("<div css={{color:'red'}}/>")
}
Expand All @@ -198,6 +198,119 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
" }\n")
}

fun testArgumentNestedInjectionBeforeProperty() {
doTest("const ErrorDiv = styled.div`\n" +
" \${props =>\n" +
" css`\n" +
" color: red; \n" +
" `}\n" +
" color: blue; \n" +
"`;", "div {\n" +
" \n" +
" color: blue; \n" +
"}", "div {\n" +
" color: red; \n" +
" }"
)
}

fun testArgumentNestedInjectionAfterProperty() {
doTest("const ErrorDiv = styled.div`\n" +
" color: blue;\n" +
" \${props =>\n" +
" css`\n" +
" color: red;\n" +
" `}\n" +
"`;", "div {\n" +
" color: blue;\n" +
" \n" +
"}", "div {\n" +
" color: red;\n" +
" }"
)
}

fun testArgumentNestedInjectionOnlyArgument() {
doTest("const OptionLabel = styled.div`\n" +
" \${(props) => css`\n" +
" margin-bottom: 0.3em;\n" +
" `}\n" +
"`;", "div {\n" +
" \n" +
"}", "div {\n" +
" margin-bottom: 0.3em;\n" +
" }"
)
}

fun testArgumentNestedInjectionLeadingArgument() {
doTest("const OptionLabel = styled.div`\${(props) => css`margin-bottom: 0.3em;`} `;",
"div { }",
"div {margin-bottom: 0.3em;}"
)
}

fun testArgumentNestedInjectionTrailingArgument() {
doTest("const OptionLabel = styled.div` \${(props) => css`margin-bottom: 0.3em;`}`;",
"div { }",
"div {margin-bottom: 0.3em;}"
)
}

fun testArgumentNestedInjectionLeadingAndTrailingArgument() {
doTest("const OptionLabel = styled.div`\${(props) => css`margin-bottom: 0.3em;`} padding: \${(props) => `5px;`}`;",
"div { padding: EXTERNAL_FRAGMENT}",
"div {margin-bottom: 0.3em;}"
)
}

fun testArgumentNestedInjectionAdjacentArguments() {
doTest("const OptionLabel = styled.div`padding: 3px; " +
"\${(props) => css`margin-bottom: 0.3em;`}\${'IGNORED'}\${props => 'display'}: none;`;",
"div {padding: 3px; EXTERNAL_FRAGMENT: none;}",
"div {margin-bottom: 0.3em;}"
)
}

fun testArgumentNestedInjectionAdjacentArgumentsLeading() {
doTest("const OptionLabel = styled.div`" +
"\${(props) => css`margin-bottom: 0.3em;`}\${'margin'}: 20px;\${props => 'display'}: none;`;",
"div {EXTERNAL_FRAGMENT: 20px;EXTERNAL_FRAGMENT: none;}",
"div {margin-bottom: 0.3em;}"
)
}

fun testArgumentNestedInjectionAdjacentArgumentsTrailing() {
doTest("const OptionLabel = styled.div`padding: 3px; " +
"\${(props) => css`margin-bottom: 0.3em;`}\${'IGNORED'}\${props => 'display'}: none; \${'background: red'}`;",
"div {padding: 3px; EXTERNAL_FRAGMENT: none; EXTERNAL_FRAGMENT}",
"div {margin-bottom: 0.3em;}"
)
}

fun testArgumentNestedInjectionAdjacentArgumentsWithInjectionInBetween() {
doTest("const OptionLabel = styled.div`padding: 3px; " +
"\${(props) => css`margin-bottom: 0.3em;`}\${(props) => css`margin-top: 0.3em;`}\${props => 'display'}: none;`;",
"div {padding: 3px; EXTERNAL_FRAGMENT: none;}",
"div {margin-bottom: 0.3em;}",
"div {margin-top: 0.3em;}"
)
}

fun testArgumentNestedPlainStringCss() {
doTest("const ErrorDiv = styled.div`\n" +
" \${props =>\n" +
" `\n" +
" color: red; \n" +
" `};\n" +
" color: blue; \n" +
"`;", "div {\n" +
" EXTERNAL_FRAGMENT;\n" +
" color: blue; \n" +
"}"
)
}

private fun setCustomInjectionsConfiguration(vararg prefixes: String) {
val configuration = CustomInjectionsConfiguration.instance(myFixture.project)
val previousPrefixes = configuration.getTagPrefixes()
Expand All @@ -221,7 +334,7 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
}

private fun collectInjectedPsiFiles(file: PsiFile): List<PsiElement> {
val result = ContainerUtil.newLinkedHashSet<PsiFile>()
val result = LinkedHashSet<PsiFile>()
PsiTreeUtil.processElements(file) {
val host = it as? PsiLanguageInjectionHost
if (host != null) {
Expand All @@ -231,6 +344,6 @@ class InjectionTest : LightPlatformCodeInsightFixtureTestCase() {
true
}

return ContainerUtil.newArrayList<PsiElement>(result)
return ArrayList(result)
}
}

0 comments on commit 7d3f4a1

Please sign in to comment.