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