diff --git a/.gitignore b/.gitignore
index 9fb18b4..3ad81cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
.idea
out
+.gradle
+build
+gradle.properties
\ No newline at end of file
diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml
deleted file mode 100644
index 4e271e2..0000000
--- a/META-INF/plugin.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
- com.deadlock.scsyntax
- Styled Components
- 1.0
- Kodehouse
-
-
- com.intellij.modules.lang
- com.intellij.css
-
- JavaScript
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..a171634
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,21 @@
+plugins {
+ id 'org.jetbrains.intellij' version '0.2.16'
+ id 'org.jetbrains.kotlin.jvm' version '1.1.3'
+}
+
+repositories {
+ mavenCentral()
+}
+
+intellij {
+ version 'IU-2017.2'
+ plugins = ['JavaScriptLanguage', 'CSS', 'less']
+ pluginName 'webstorm-styled-components'
+ downloadSources false
+}
+
+sourceSets {
+ test {
+ java.srcDir 'src/test'
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..b73f054
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..84cc5fb
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Aug 31 03:30:38 MSK 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4453cce
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..20abeeb
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'webstorm-styled-components'
\ No newline at end of file
diff --git a/src/SCInjector.java b/src/SCInjector.java
deleted file mode 100644
index 301b96f..0000000
--- a/src/SCInjector.java
+++ /dev/null
@@ -1,71 +0,0 @@
-import com.intellij.lang.css.CSSLanguage;
-import com.intellij.lang.ecmascript6.ES6ElementTypes;
-import com.intellij.lang.javascript.JSTokenTypes;
-import com.intellij.lang.javascript.psi.JSReferenceExpression;
-import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression;
-import com.intellij.psi.*;
-import com.intellij.psi.util.PsiTreeUtil;
-import java.util.ArrayList;
-import java.util.Collection;
-import com.intellij.openapi.util.TextRange;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Created by deadlock on 8/30/17.
- */
-public class SCInjector implements LanguageInjector {
- @Override
- public void getLanguagesToInject(@NotNull PsiLanguageInjectionHost psiLanguageInjectionHost, @NotNull InjectedLanguagePlaces injectedLanguagePlaces) {
- PsiElement element = psiLanguageInjectionHost;
-
- if (element.getParent().getNode().getElementType() != ES6ElementTypes.TAGGED_TEMPLATE_EXPRESSION) {
- return;
- }
-
- String parentExpressionText = element.getParent().getFirstChild().getText();
- if (!parentExpressionText.startsWith("styled") && !parentExpressionText.startsWith("keyframes") && !parentExpressionText.endsWith("extend")) {
- return;
- }
-
- Collection refs = PsiTreeUtil.findChildrenOfAnyType(element, PsiElement.class);
- for (PsiElement c : refs) {
- if (c.getNode().getElementType() == JSTokenTypes.STRING_TEMPLATE_PART) {
- TextRange range = new TextRange(c.getStartOffsetInParent(), c.getStartOffsetInParent() + c.getTextLength());
- System.out.println(c.getText());
- String prefix = c.getText().trim().indexOf(":") < c.getText().trim().indexOf(";") &&
- c.getText().trim().indexOf(":") != -1 &&
- c.getText().trim().indexOf(";") != -1 ? "sel {" : "sel { fakeprop: initial ";
-
- if (c.getText().trim().endsWith(":")) {
- prefix = "sel {";
- }
- String suffix = c.getText().trim().endsWith(";") ? "}" : "initial; }";
- injectedLanguagePlaces.addPlace(CSSLanguage.findLanguageByID("SCSS"), range, prefix, suffix);
- }
- }
- }
-
- Collection findSCBlocks(PsiElement file) {
- Collection templates = PsiTreeUtil.findChildrenOfType(file, JSStringTemplateExpression.class);
- Collection blocks = new ArrayList();
- for (PsiElement template : templates) {
- Collection refs = PsiTreeUtil.findChildrenOfAnyType(template, JSReferenceExpression.class);
- if (refs.size() == 0) {
- templates.remove(template);
- }
- }
-
- for (PsiElement template : templates) {
- //Get css block
- Collection scBlocks = PsiTreeUtil.findChildrenOfType(template, PsiElement.class);
- for (PsiElement e : scBlocks) {
- if (e.getNode().getElementType() == JSTokenTypes.STRING_TEMPLATE_PART) {
- blocks.add(e);
- }
- }
- }
-
-
- return blocks;
- }
-}
diff --git a/src/main/kotlin/com/intellij/StyledComponents/InjectionUtils.kt b/src/main/kotlin/com/intellij/StyledComponents/InjectionUtils.kt
new file mode 100644
index 0000000..fb5671f
--- /dev/null
+++ b/src/main/kotlin/com/intellij/StyledComponents/InjectionUtils.kt
@@ -0,0 +1,41 @@
+package com.intellij.StyledComponents
+
+import com.intellij.lang.javascript.JSTokenTypes
+import com.intellij.lang.javascript.psi.JSLiteralExpression
+import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.util.TextRange
+import com.intellij.util.ArrayUtil
+import com.intellij.util.containers.ContainerUtil
+import java.util.ArrayList
+
+val INJECTED_FILE_RANGES_KEY = Key?>("INJECTED_FILE_RANGES_KEY")
+private val EXTERNAL_FRAGMENT = "EXTERNAL_FRAGMENT"
+
+fun getInjectionPlaces(myQuotedLiteral: JSLiteralExpression): List {
+ if (myQuotedLiteral is JSStringTemplateExpression) {
+ val templateExpression = myQuotedLiteral
+ val ranges = templateExpression.stringRanges
+ if (ranges.isNotEmpty()) {
+ val result = ArrayList(ranges.size)
+ val quotedLiteralNode = myQuotedLiteral.getNode()
+ val backquote = quotedLiteralNode.findChildByType(JSTokenTypes.BACKQUOTE)
+ val backquoteOffset = if (backquote != null) backquote.startOffset - quotedLiteralNode.startOffset else -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
+ result.add(StringPlace(prefix, range, suffix))
+ }
+ return result
+ }
+ if (!ArrayUtil.isEmpty(templateExpression.arguments)) {
+ return ContainerUtil.emptyList()
+ }
+ }
+ val endOffset = Math.max(myQuotedLiteral.textLength - 1, 1)
+ return listOf(StringPlace(null, TextRange.create(1, endOffset), null))
+}
+
+
+data class StringPlace(val prefix: String?, val range: TextRange, val suffix: String?)
\ No newline at end of file
diff --git a/src/main/kotlin/com/intellij/StyledComponents/InterpolationArgumentsErrorFilter.kt b/src/main/kotlin/com/intellij/StyledComponents/InterpolationArgumentsErrorFilter.kt
new file mode 100644
index 0000000..ef113a4
--- /dev/null
+++ b/src/main/kotlin/com/intellij/StyledComponents/InterpolationArgumentsErrorFilter.kt
@@ -0,0 +1,36 @@
+package com.intellij.StyledComponents
+
+import com.intellij.codeInsight.daemon.impl.HighlightInfo
+import com.intellij.codeInsight.daemon.impl.HighlightInfoFilter
+import com.intellij.codeInsight.highlighting.HighlightErrorFilter
+import com.intellij.lang.annotation.HighlightSeverity
+import com.intellij.psi.PsiErrorElement
+import com.intellij.psi.PsiFile
+
+class InterpolationArgumentsErrorFilter : HighlightErrorFilter(), HighlightInfoFilter {
+ override fun shouldHighlightErrorElement(element: PsiErrorElement): Boolean {
+ val acceptedRanges = element.containingFile.getUserData(INJECTED_FILE_RANGES_KEY)
+ if (acceptedRanges == null) {
+ return true
+ }
+ return acceptedRanges.any { range -> range.contains(element.textRange) }
+ }
+
+ override fun accept(highlightInfo: HighlightInfo, file: PsiFile?): Boolean {
+ val acceptedRanges = file?.getUserData(INJECTED_FILE_RANGES_KEY)
+ if (acceptedRanges == null) {
+ return true
+ }
+ if (highlightInfo.severity === HighlightSeverity.WARNING
+ || highlightInfo.severity === HighlightSeverity.WEAK_WARNING
+ || highlightInfo.severity === HighlightSeverity.ERROR) {
+ return acceptedRanges.any { range ->
+ highlightInfo.startOffset > range.startOffset
+ && highlightInfo.endOffset < range.endOffset
+ }
+ }
+ return true
+ }
+
+}
+
diff --git a/src/main/kotlin/com/intellij/StyledComponents/Patterns.kt b/src/main/kotlin/com/intellij/StyledComponents/Patterns.kt
new file mode 100644
index 0000000..dfcd44e
--- /dev/null
+++ b/src/main/kotlin/com/intellij/StyledComponents/Patterns.kt
@@ -0,0 +1,58 @@
+package com.intellij.StyledComponents
+
+import com.intellij.lang.javascript.psi.JSExpression
+import com.intellij.lang.javascript.psi.JSReferenceExpression
+import com.intellij.lang.javascript.psi.ecma6.ES6TaggedTemplateExpression
+import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.patterns.ElementPattern
+import com.intellij.patterns.PatternCondition
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.util.ProcessingContext
+import com.intellij.util.SmartList
+import com.intellij.util.containers.ContainerUtil
+import java.util.*
+
+fun taggedTemplate(name: String): ElementPattern {
+ return taggedTemplate(referenceExpression().withText(name))
+}
+
+fun taggedTemplate(tagPattern: ElementPattern): ElementPattern {
+ return PlatformPatterns.psiElement(JSStringTemplateExpression::class.java)
+ .withParent(PlatformPatterns.psiElement(ES6TaggedTemplateExpression::class.java)
+ .withChild(tagPattern))
+}
+
+fun withReferenceName(name: String): ElementPattern {
+ return referenceExpression()
+ .with(object : PatternCondition("referenceName") {
+ override fun accepts(referenceExpression: JSReferenceExpression, context: ProcessingContext): Boolean {
+ return StringUtil.equals(referenceExpression.referenceName, name)
+ }
+ })
+}
+
+fun referenceExpression() = PlatformPatterns.psiElement(JSReferenceExpression::class.java)!!
+
+fun withNameStartingWith(vararg components: String): ElementPattern {
+ val componentsList = ContainerUtil.list(*components)
+ return referenceExpression().with(object : PatternCondition("nameStartingWith") {
+ override fun accepts(referenceExpression: JSReferenceExpression, context: ProcessingContext): Boolean {
+ return ContainerUtil.startsWith(getReferenceParts(referenceExpression), componentsList)
+ }
+ })
+}
+
+fun getReferenceParts(jsReferenceExpression: JSReferenceExpression): List {
+ val nameParts = SmartList()
+
+ var ref: JSReferenceExpression? = jsReferenceExpression
+ while (ref != null) {
+ val name = ref.referenceName
+ if (StringUtil.isEmptyOrSpaces(name)) return ContainerUtil.emptyList()
+ nameParts.add(name)
+ ref = ref.qualifier as JSReferenceExpression?
+ }
+ Collections.reverse(nameParts)
+ return nameParts
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/intellij/StyledComponents/StyledComponentsInjector.kt b/src/main/kotlin/com/intellij/StyledComponents/StyledComponentsInjector.kt
new file mode 100644
index 0000000..5e560a0
--- /dev/null
+++ b/src/main/kotlin/com/intellij/StyledComponents/StyledComponentsInjector.kt
@@ -0,0 +1,66 @@
+package com.intellij.StyledComponents
+
+import com.intellij.lang.injection.MultiHostInjector
+import com.intellij.lang.injection.MultiHostRegistrar
+import com.intellij.lang.javascript.psi.JSExpression
+import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression
+import com.intellij.openapi.util.Pair
+import com.intellij.openapi.util.TextRange
+import com.intellij.patterns.ElementPattern
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import com.intellij.psi.impl.source.tree.injected.MultiHostRegistrarImpl
+import com.intellij.psi.impl.source.tree.injected.Place
+import org.jetbrains.plugins.less.LESSLanguage
+
+class StyledComponentsInjector : MultiHostInjector {
+ companion object {
+ private val styledPattern = withNameStartingWith("styled")
+ val places: List = listOf(
+ PlaceInfo(taggedTemplate(PlatformPatterns.or(styledPattern,
+ PlatformPatterns.psiElement(JSExpression::class.java)
+ .withFirstChild(styledPattern))), "div {", "}"),
+ PlaceInfo(taggedTemplate(withReferenceName("extend")), "div {", "}"),
+ PlaceInfo(taggedTemplate("css")),
+ PlaceInfo(taggedTemplate("injectGlobal")),
+ PlaceInfo(taggedTemplate("keyframes"), "@keyframes foo {", "}")
+ )
+ }
+
+ override fun elementsToInjectIn(): MutableList> {
+ return mutableListOf(JSStringTemplateExpression::class.java)
+ }
+
+ override fun getLanguagesToInject(registrar: MultiHostRegistrar, injectionHost: PsiElement) {
+ if (injectionHost !is JSStringTemplateExpression)
+ return
+ val acceptedPattern = places.find { (elementPattern) -> elementPattern.accepts(injectionHost) }
+ if (acceptedPattern != null) {
+ val stringPlaces = getInjectionPlaces(injectionHost)
+ registrar.startInjecting(LESSLanguage.INSTANCE)
+ stringPlaces.forEachIndexed { index, (prefix, range, suffix) ->
+ val thePrefix = if (index == 0) acceptedPattern.prefix else prefix
+ val theSuffix = if (index == stringPlaces.size - 1) acceptedPattern.suffix else suffix
+ registrar.addPlace(thePrefix, theSuffix, injectionHost, range)
+ }
+ registrar.doneInjecting()
+ val result = getInjectionResult(registrar) ?: return
+ val injectedFile = result.second
+ val injectedFileRanges = result.first.map { TextRange(it.range.startOffset, it.range.endOffset - it.suffix.length) }
+
+ if (injectedFileRanges.size > 1) {
+ injectedFile.putUserData(INJECTED_FILE_RANGES_KEY, injectedFileRanges)
+ }
+ }
+ }
+
+ private fun getInjectionResult(registrar: MultiHostRegistrar): Pair? {
+ val result = (registrar as MultiHostRegistrarImpl).result
+ return if (result == null || result.isEmpty()) null
+ else result[result.size - 1]
+ }
+ data class PlaceInfo(val elementPattern: ElementPattern,
+ val prefix: String? = null,
+ val suffix: String? = null)
+}
\ No newline at end of file
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
new file mode 100644
index 0000000..10bd0e9
--- /dev/null
+++ b/src/main/resources/META-INF/plugin.xml
@@ -0,0 +1,30 @@
+
+ com.deadlock.scsyntax
+ Styled Components
+ 1.0
+ Kodehouse
+
+
+ com.intellij.modules.lang
+ com.intellij.css
+
+ JavaScript
+ org.jetbrains.plugins.less
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/CompletionTest.kt b/src/test/CompletionTest.kt
new file mode 100644
index 0000000..16f0293
--- /dev/null
+++ b/src/test/CompletionTest.kt
@@ -0,0 +1,18 @@
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
+
+class CompletionTest : LightCodeInsightFixtureTestCase() {
+ fun testCompletionAfterInterpolationExpressionInParentheses() {
+ myFixture.configureByText("dummy.es6",
+ "const HeroImage = styled(Box)`\n" +
+ " position: relative;\n" +
+ " &:after {\n" +
+ " position: abs\${'out'}te;\n" +
+ " background-image: url('\${p => p.bgSrc}');" +
+ " border-radius: 6px;\n" +
+ " color: \n" +
+ " }\n" +
+ "`")
+ val lookupElements = myFixture.completeBasic().map { it.lookupString }
+ assertContainsElements(lookupElements, "red", "blue")
+ }
+}
diff --git a/src/test/HighlightingTest.kt b/src/test/HighlightingTest.kt
new file mode 100644
index 0000000..6282c6e
--- /dev/null
+++ b/src/test/HighlightingTest.kt
@@ -0,0 +1,43 @@
+import com.intellij.psi.css.inspections.invalid.CssInvalidPropertyValueInspection
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
+
+class HighlightingTest : LightCodeInsightFixtureTestCase() {
+
+ fun testWithoutArguments_ErrorsHighlighted() {
+ myFixture.enableInspections(CssInvalidPropertyValueInspection::class.java)
+ doTest("var someCss = css`div {\n" +
+ " color: {}\n" +
+ "};`")
+ }
+
+ fun testErrorSurroundsInterpolationArgument_NotHighlighted() {
+ myFixture.enableInspections(CssInvalidPropertyValueInspection::class.java)
+ doTest("var someCss = css`\n" +
+ "//should not highlight\n" +
+ "withArgument{\n" +
+ " border: 5px \${foobar} red;\n" +
+ "},\n" +
+ "//should highlight\n" +
+ "withoutArgument {\n" +
+ " border: 5px foobar-not-acceptable red;\n" +
+ "};;`")
+ }
+
+ fun testErrorAdjacentToInterpolationArgument_NotHighlighted() {
+ doTest("var styledSomething = styled.something`\n" +
+ " perspective: 1000px;\n" +
+ " \${value}\n" +
+ " \${anotherValue}\n" +
+ "`\n" +
+ "const Triangle = styled.span`\n" +
+ " \${({ right }) => (right ? 'right: 0;' : 'left: 0;')}\n" +
+ "`")
+ }
+
+ private fun doTest(expected: String) {
+ myFixture.setCaresAboutInjection(false)
+ myFixture.configureByText("dummy.es6", expected)
+ myFixture.testHighlighting(true, false, true)
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/InjectionTest.kt b/src/test/InjectionTest.kt
new file mode 100644
index 0000000..c2365e8
--- /dev/null
+++ b/src/test/InjectionTest.kt
@@ -0,0 +1,121 @@
+import com.intellij.lang.javascript.psi.JSRecursiveElementVisitor
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiLanguageInjectionHost
+import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
+import com.intellij.util.containers.ContainerUtil
+import org.junit.Assert
+
+class InjectionTest : LightCodeInsightFixtureTestCase() {
+
+ fun testSimpleComponent() {
+ doTest("const Title = styled.h1`\n" +
+ " font-size: 1.5em;\n" +
+ "`;",
+ "div {\n" +
+ " font-size: 1.5em;\n" +
+ "}")
+ }
+
+ fun testComponentWithArgs() {
+ doTest("const Button = styled.button`\n" +
+ " /* Adapt the colours based on primary prop */\n" +
+ " background: \${props => props.primary ? 'palevioletred' : 'white'};\n" +
+ " 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" +
+ "}")
+ }
+
+ fun testComplexExpression() {
+ doTest("const Input = styled.input.attrs({\n" +
+ " type: 'password',\n" +
+ "\n" +
+ " // or we can define dynamic ones\n" +
+ " margin: props => props.size || '1em',\n" +
+ " padding: props => props.size || '1em',\n" +
+ "})`\n" +
+ " color: palevioletred;\n" +
+ "`;", "div {\n" +
+ " color: palevioletred;\n" +
+ "}")
+ }
+
+ fun testKeyframes() {
+ doTest("const rotate360 = keyframes`\n" +
+ " from{\n" +
+ " transform:rotate(0deg);\n" +
+ " }\n" +
+ "\n" +
+ " to{\n" +
+ " transform:rotate(360deg);\n" +
+ " }\n" +
+ "`;" +
+ "`;",
+ "@keyframes foo {\n" +
+ " from{\n" +
+ " transform:rotate(0deg);\n" +
+ " }\n" +
+ "\n" +
+ " to{\n" +
+ " transform:rotate(360deg);\n" +
+ " }\n" +
+ "}")
+
+ }
+
+ fun testExtendsComponent() {
+ doTest("const TomatoButton = Button.extend`\n" +
+ " color: tomato;\n" +
+ " border-color: tomato;\n" +
+ "`;",
+ "div {\n" +
+ " color: tomato;\n" +
+ " border-color: tomato;\n" +
+ "}")
+ }
+
+ fun testInjectGlobal() {
+ doTest("injectGlobal`\n" +
+ " div{\n" +
+ " color:red\n" +
+ " }\n" +
+ "`", "\n" +
+ " div{\n" +
+ " color:red\n" +
+ " }\n")
+ }
+
+ private fun doTest(fileContent: String, expected: String) {
+ myFixture.setCaresAboutInjection(true)
+ val file = myFixture.configureByText("dummy.es6", fileContent)
+ val injections = collectInjectedPsiContents(file)
+ Assert.assertEquals(1, injections.size)
+ Assert.assertEquals(expected, injections[0])
+ }
+
+ private fun collectInjectedPsiContents(file: PsiFile): List {
+ return ContainerUtil.map(collectInjectedPsiFiles(file)) { element -> element.text }
+ }
+
+ private fun collectInjectedPsiFiles(file: PsiFile): List {
+ val result = ContainerUtil.newLinkedHashSet()
+ file.accept(object : JSRecursiveElementVisitor() {
+ override fun visitElement(element: PsiElement) {
+ super.visitElement(element)
+ val host = element as? PsiLanguageInjectionHost
+ if (host != null) {
+ InjectedLanguageUtil.enumerate(host) { injectedPsi, _ -> result.add(injectedPsi) }
+ }
+ }
+ })
+ return ContainerUtil.newArrayList(result)
+ }
+}
diff --git a/webstorm-styled-components.jar b/webstorm-styled-components.jar
deleted file mode 100644
index 1fd7e30..0000000
Binary files a/webstorm-styled-components.jar and /dev/null differ
diff --git a/webstorm-styled-components.zip b/webstorm-styled-components.zip
new file mode 100644
index 0000000..1a515ea
Binary files /dev/null and b/webstorm-styled-components.zip differ