├── settings.gradle ├── plugin-content.yaml ├── screenshot.png ├── docs ├── additional_tags.png └── error_after_argument.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── testData └── completion │ ├── attrClassNames.js │ ├── attrClassNamesFunc.js │ ├── styledComponentsStylesheet.css │ └── attrClassNamesNestedExpression.js ├── src ├── main │ ├── kotlin │ │ └── com │ │ │ └── intellij │ │ │ └── styledComponents │ │ │ ├── PlaceInfo.kt │ │ │ ├── StyledComponentsBundle.kt │ │ │ ├── StyledComponentsErrorFilter.kt │ │ │ ├── InjectionUtils.kt │ │ │ ├── CustomInjectionsConfiguration.kt │ │ │ ├── StyledComponentsReferenceContributor.kt │ │ │ ├── CssPropAttributeDescriptorProvider.kt │ │ │ ├── StyledComponentsInjector.kt │ │ │ ├── Patterns.kt │ │ │ └── StyledComponentsConfigurable.kt │ └── resources │ │ ├── messages │ │ └── StyledComponentsBundle.properties │ │ └── META-INF │ │ ├── CHANGELOG.md │ │ ├── plugin.xml │ │ └── pluginIcon.svg └── test │ └── com │ └── intellij │ └── styledComponents │ ├── HighlightingTest.kt │ ├── CompletionTest.kt │ └── InjectionTest.kt ├── .github └── ISSUE_TEMPLATE.md ├── LICENSE.md ├── intellij.styled.components.iml ├── gradlew.bat ├── BUILD.bazel ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'webstorm-styled-components' -------------------------------------------------------------------------------- /plugin-content.yaml: -------------------------------------------------------------------------------- 1 | - name: lib/styled-components.jar 2 | modules: 3 | - name: intellij.styled.components -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/styled-components/webstorm-styled-components/HEAD/screenshot.png -------------------------------------------------------------------------------- /docs/additional_tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/styled-components/webstorm-styled-components/HEAD/docs/additional_tags.png -------------------------------------------------------------------------------- /docs/error_after_argument.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/styled-components/webstorm-styled-components/HEAD/docs/error_after_argument.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/styled-components/webstorm-styled-components/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /testData/completion/attrClassNames.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Component = styled.main.attrs({ 4 | className: "la" 5 | })``; -------------------------------------------------------------------------------- /testData/completion/attrClassNamesFunc.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Component = styled.input.attrs(props => ({ 4 | className: `col` 5 | }))``; -------------------------------------------------------------------------------- /testData/completion/styledComponentsStylesheet.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | } 3 | 4 | .layout-wide { 5 | } 6 | 7 | .layout-narrow { 8 | } 9 | 10 | .col-xs { 11 | } 12 | 13 | .col-sm { 14 | } -------------------------------------------------------------------------------- /testData/completion/attrClassNamesNestedExpression.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Component = styled.main.attrs({ 4 | className: 1 > 2 ? "layout-" : "" 5 | })``; -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/PlaceInfo.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.patterns.ElementPattern 4 | import com.intellij.psi.PsiElement 5 | 6 | data class PlaceInfo(val elementPattern: ElementPattern, 7 | val prefix: String = "", 8 | val suffix: String = "") -------------------------------------------------------------------------------- /src/main/resources/messages/StyledComponentsBundle.properties: -------------------------------------------------------------------------------- 1 | styled.components.configurable.title=Styled Components 2 | styled.components.configurable.label.tag.prefixes=Additional template tag prefixes: 3 | styled.components.configurable.error.value.is.empty=Value is empty 4 | styled.components.configurable.not.valid.identifier=''{0}'' is not a valid JavaScript identifier 5 | styled.components.configurable.not.valid.property.name=''{0}'' is not a valid property name 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/StyledComponentsBundle.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.Nls 5 | import org.jetbrains.annotations.NonNls 6 | import org.jetbrains.annotations.PropertyKey 7 | 8 | @NonNls 9 | private const val BUNDLE = "messages.StyledComponentsBundle" 10 | 11 | object StyledComponentsBundle : DynamicBundle(BUNDLE) { 12 | @Nls 13 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String = getMessage(key, *params) 14 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | =============================================================================== 3 | 4 | ## v.1.0.9 5 | Added support for [CSS prop](https://www.styled-components.com/docs/api#css-prop) 6 | 7 | ## v.1.0.8 8 | Custom patterns now apply to simple reference expressions also. 9 | 10 | ## v.1.0.7 11 | 12 | * Added support for [configuring custom patterns](https://github.com/styled-components/webstorm-styled-components/#configuration) in IDE preferences. 13 | * Minimum IDE version is now 2018.1 14 | 15 | ## v.1.0 16 | Initial version -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | --- 4 | 5 | 10 | 11 | * **IDE name and version:** 12 | 13 | * **Styled-components plugin version:** 14 | 15 | 16 | ## Problem description: 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Hossam Saraya 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/StyledComponentsErrorFilter.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.codeInsight.highlighting.HighlightErrorFilter 4 | import com.intellij.lang.Language 5 | import com.intellij.lang.injection.MultiHostRegistrar 6 | import com.intellij.openapi.util.Key 7 | import com.intellij.psi.PsiErrorElement 8 | import com.intellij.psi.PsiLanguageInjectionHost 9 | import com.intellij.psi.css.CssDeclaration 10 | import com.intellij.psi.css.CssTerm 11 | import com.intellij.psi.css.impl.CssElementTypes 12 | import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil 13 | import com.intellij.psi.util.PsiTreeUtil 14 | import com.intellij.psi.util.PsiUtilCore 15 | 16 | internal class StyledComponentsErrorFilter : HighlightErrorFilter() { 17 | companion object { 18 | private val STYLED_COMPONENTS_INJECTION = Key.create("styled.components.injection") 19 | 20 | fun register(registrar: MultiHostRegistrar) { 21 | registrar.putInjectedFileUserData(STYLED_COMPONENTS_INJECTION, true) 22 | } 23 | } 24 | 25 | override fun shouldHighlightErrorElement(element: PsiErrorElement): Boolean { 26 | if (!STYLED_COMPONENTS_INJECTION.get(element.containingFile, false)) return true 27 | if (PsiUtilCore.getElementType(PsiTreeUtil.skipWhitespacesAndCommentsForward(element)) != CssElementTypes.CSS_COLON) return true 28 | 29 | val prevElement = PsiTreeUtil.skipWhitespacesAndCommentsBackward(element)?.takeIf { it is CssTerm } ?: return true 30 | val declaration = prevElement.parent?.parent as? CssDeclaration ?: return true 31 | 32 | return !declaration.propertyName.startsWith(EXTERNAL_FRAGMENT) 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/InjectionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.javascript.psi.JSExpression 4 | import com.intellij.lang.javascript.psi.JSLiteralExpression 5 | import com.intellij.lang.javascript.psi.JSReferenceExpression 6 | import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression 7 | import com.intellij.openapi.util.TextRange 8 | import com.intellij.psi.PsiElement 9 | import kotlin.math.max 10 | 11 | const val EXTERNAL_FRAGMENT = "EXTERNAL_FRAGMENT" 12 | 13 | fun getInjectionPlaces(quotedLiteral: PsiElement): List { 14 | if (quotedLiteral is JSStringTemplateExpression) { 15 | val ranges = quotedLiteral.stringRangesWithEmpty 16 | val arguments = quotedLiteral.arguments 17 | 18 | // `${css`margin: none;`}` and `` 19 | if (ranges.size <= 2 && ranges.all { it.isEmpty }) { 20 | return emptyList() 21 | } 22 | 23 | return ranges.mapIndexed { i, textRange -> 24 | StringPlace(null, textRange, getArgumentPlaceholder(arguments.elementAtOrNull(i), i)) 25 | } 26 | } 27 | 28 | val endOffset = max(quotedLiteral.textLength - 1, 1) 29 | return listOf(StringPlace(null, TextRange.create(1, endOffset), null)) 30 | } 31 | 32 | private fun getArgumentPlaceholder(argument: JSExpression?, index: Int): String? { 33 | if (argument == null) return null 34 | 35 | if (argument is JSLiteralExpression) { 36 | val value = argument.value 37 | if (value != null) { 38 | return value.toString() 39 | } 40 | } 41 | 42 | if (argument is JSReferenceExpression && argument.qualifier == null) { 43 | val referenceName = argument.referenceName 44 | if (referenceName != null) { 45 | return referenceName 46 | } 47 | } 48 | 49 | return "${EXTERNAL_FRAGMENT}_$index" 50 | } 51 | 52 | data class StringPlace(val prefix: String?, val range: TextRange, val suffix: String?) -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.deadlock.scsyntax 3 | Styled Components & Styled JSX 4 | JavaScript Frameworks and Tools 5 | JetBrains 6 | com.intellij.modules.lang 7 | com.intellij.css 8 | JavaScript 9 | org.intellij.plugins.postcss 10 | Adds support for styled-components and styled-jsx. 12 |
    13 |
  • Code completion for CSS properties and values inside template literals. 14 |
  • Various quick fixes and intentions for CSS when you press Alt-Enter. 15 |
  • Completion suggestions for JavaScript variables, methods, and functions and navigation to their definitions with Cmd/Ctrl-click. 16 | ]]> 17 | messages.StyledComponentsBundle 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/test/com/intellij/styledComponents/HighlightingTest.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.psi.css.inspections.CssUnknownPropertyInspection 4 | import com.intellij.psi.css.inspections.invalid.CssInvalidPropertyValueInspection 5 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 6 | 7 | class HighlightingTest : BasePlatformTestCase() { 8 | 9 | fun testWithoutArguments_ErrorsHighlighted() { 10 | myFixture.enableInspections(CssInvalidPropertyValueInspection::class.java, CssUnknownPropertyInspection::class.java) 11 | doTest(""" 12 | var someCss = css`div { 13 | color: not-a-color; 14 | .nested { 15 | unknown: 0; 16 | } 17 | @container sidebar (width < calc(64px + 12ch)) { 18 | display: none; 19 | } 20 | }` 21 | """.trimIndent()) 22 | } 23 | 24 | fun testErrorSurroundsInterpolationArgument_NotHighlighted() { 25 | myFixture.enableInspections(CssInvalidPropertyValueInspection::class.java) 26 | doTest("var someCss = css`\n" + 27 | "//should not highlight\n" + 28 | "withArgument{\n" + 29 | " border: 5px \${foobar} red;\n" + 30 | "},\n" + 31 | "//should highlight\n" + 32 | "withoutArgument {\n" + 33 | " border: 5px foobar-not-acceptable red;\n" + 34 | "};;`") 35 | } 36 | 37 | fun testErrorAdjacentToInterpolationArgument_NotHighlighted() { 38 | doTest("var styledSomething = styled.something`\n" + 39 | " perspective: 1000px;\n" + 40 | " \${value}\n" + 41 | " \${anotherValue}\n" + 42 | "`\n" + 43 | "const Triangle = styled.span`\n" + 44 | " \${({ right }) => (right ? 'right: 0;' : 'left: 0;')}\n" + 45 | "`") 46 | } 47 | 48 | private fun doTest(expected: String) { 49 | myFixture.setCaresAboutInjection(false) 50 | myFixture.configureByText("dummy.es6", expected) 51 | myFixture.testHighlighting(true, false, true) 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/CustomInjectionsConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.javascript.psi.JSExpression 4 | import com.intellij.openapi.components.PersistentStateComponent 5 | import com.intellij.openapi.components.Service 6 | import com.intellij.openapi.components.State 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.patterns.PlatformPatterns 9 | 10 | @Service(Service.Level.PROJECT) 11 | @State(name = "StyledComponentsInjections") 12 | class CustomInjectionsConfiguration : PersistentStateComponent { 13 | private var myState: InjectionsState? = null 14 | private var myPatterns: List = emptyList() 15 | 16 | override fun getState(): InjectionsState? { 17 | return myState 18 | } 19 | 20 | override fun loadState(newState: InjectionsState) { 21 | updatePatterns(newState) 22 | myState = newState 23 | } 24 | 25 | private fun updatePatterns(newState: InjectionsState) { 26 | myPatterns = (newState.prefixes ?: emptyArray()).map { 27 | val referenceExpressionPattern = withNameStartingWith(it.trim().split('.')) 28 | val tagPattern = PlatformPatterns.or( 29 | referenceExpressionPattern, 30 | PlatformPatterns.psiElement(JSExpression::class.java).withFirstChild(referenceExpressionPattern) 31 | ) 32 | PlaceInfo(taggedTemplate(tagPattern), COMPONENT_PROPS_PREFIX, COMPONENT_PROPS_SUFFIX) 33 | } 34 | } 35 | 36 | fun getInjectionPlaces(): List { 37 | return myPatterns 38 | } 39 | 40 | fun getTagPrefixes(): Array { 41 | return myState?.prefixes ?: emptyArray() 42 | } 43 | 44 | fun setTagPrefixes(prefixes: Array) { 45 | val newState = InjectionsState(prefixes) 46 | myState = newState 47 | updatePatterns(newState) 48 | } 49 | 50 | class InjectionsState(var prefixes: Array? = null) 51 | 52 | companion object { 53 | fun instance(project: Project): CustomInjectionsConfiguration = project.getService(CustomInjectionsConfiguration::class.java) 54 | } 55 | } -------------------------------------------------------------------------------- /intellij.styled.components.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/StyledComponentsReferenceContributor.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.javascript.patterns.JSPatterns.* 4 | import com.intellij.lang.javascript.psi.JSLiteralExpression 5 | import com.intellij.lang.javascript.psi.JSLiteralExpressionKind 6 | import com.intellij.lang.javascript.psi.JSObjectLiteralExpression 7 | import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression 8 | import com.intellij.psi.* 9 | import com.intellij.psi.css.resolve.CssClassOrIdReference 10 | import com.intellij.psi.css.util.CssResolveUtil 11 | import com.intellij.psi.filters.ElementFilter 12 | import com.intellij.psi.filters.position.FilterPattern 13 | import com.intellij.util.ProcessingContext 14 | 15 | internal class StyledComponentsReferenceContributor : PsiReferenceContributor() { 16 | override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { 17 | registrar.registerReferenceProvider( 18 | jsLiteralExpression().inside( 19 | jsProperty() 20 | .withName("className") 21 | .withParent(JSObjectLiteralExpression::class.java) 22 | .inside(jsArgument(jsReferenceExpression().withReferenceName("attrs"), 0)) 23 | ).and(FilterPattern(object : ElementFilter { 24 | override fun isAcceptable(element: Any?, context: PsiElement?): Boolean { 25 | return element is JSLiteralExpression && element.isQuotedLiteral 26 | } 27 | 28 | override fun isClassAcceptable(hintClass: Class<*>?) = true 29 | })), 30 | StyledComponentsClassNamesReferenceProvider() 31 | ) 32 | } 33 | } 34 | 35 | private class StyledComponentsClassNamesReferenceProvider : PsiReferenceProvider() { 36 | override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { 37 | if (element !is JSLiteralExpression) { 38 | return emptyArray() 39 | } 40 | 41 | val references = arrayListOf() 42 | if (element.getExpressionKind(false) == JSLiteralExpressionKind.TEMPLATE_WITH_ARGS) { 43 | val templateExpression = element as JSStringTemplateExpression 44 | val text = templateExpression.text 45 | templateExpression.stringRanges.forEach { 46 | extractReferences(it.substring(text), references, element, it.startOffset) 47 | } 48 | } 49 | else { 50 | element.stringValue?.let { 51 | extractReferences(it, references, element, 1) 52 | } 53 | } 54 | 55 | return references.toTypedArray() 56 | } 57 | 58 | fun extractReferences(text: String, references: MutableList, element: PsiElement, offset: Int) { 59 | CssResolveUtil.consumeClassNames(text, element) { _, range -> 60 | references.add(object : CssClassOrIdReference(element, range.shiftRight(offset)) { 61 | override fun isId(): Boolean { 62 | return false 63 | } 64 | }) 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | ### auto-generated section `build intellij.styled.components` start 2 | load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup") 3 | 4 | resourcegroup( 5 | name = "styled-components_resources", 6 | srcs = glob(["src/main/resources/**/*"]), 7 | strip_prefix = "src/main/resources" 8 | ) 9 | 10 | resourcegroup( 11 | name = "styled-components_test_resources", 12 | srcs = glob(["testData/**/*"]), 13 | strip_prefix = "testData" 14 | ) 15 | 16 | jvm_library( 17 | name = "styled-components", 18 | module_name = "intellij.styled.components", 19 | visibility = ["//visibility:public"], 20 | srcs = glob(["src/main/kotlin/**/*.kt", "src/main/kotlin/**/*.java", "src/main/kotlin/**/*.form"], allow_empty = True), 21 | resources = [":styled-components_resources"], 22 | deps = [ 23 | "@community//platform/analysis-api:analysis", 24 | "@community//platform/projectModel-api:projectModel", 25 | "@community//platform/util", 26 | "@community//platform/platform-api:ide", 27 | "@community//platform/platform-impl:ide-impl", 28 | "@community//platform/lang-impl", 29 | "//plugins/JavaScriptLanguage/javascript-parser", 30 | "@community//xml/xml-parser:parser", 31 | "//plugins/css/common", 32 | "@community//platform/core-api:core", 33 | "//plugins/css/psi", 34 | "@community//platform/core-ui", 35 | "@community//xml/impl", 36 | "//plugins/css/plugin", 37 | "//contrib/postcss", 38 | "//plugins/JavaScriptLanguage:javascript-backend", 39 | "@community//xml/xml-psi-api:psi", 40 | "//plugins/css/common/psi", 41 | "//plugins/JavaScriptLanguage/javascript-common", 42 | ] 43 | ) 44 | 45 | jvm_library( 46 | name = "styled-components_test_lib", 47 | visibility = ["//visibility:public"], 48 | srcs = glob(["src/test/**/*.kt", "src/test/**/*.java", "src/test/**/*.form"], allow_empty = True), 49 | resources = [":styled-components_test_resources"], 50 | associates = [":styled-components"], 51 | deps = [ 52 | "@community//platform/analysis-api:analysis", 53 | "@community//platform/code-style-api:codeStyle", 54 | "@community//platform/projectModel-api:projectModel", 55 | "@community//platform/util", 56 | "@community//platform/platform-api:ide", 57 | "@community//platform/platform-impl:ide-impl", 58 | "@community//platform/lang-impl", 59 | "//plugins/JavaScriptLanguage/javascript-parser", 60 | "@community//xml/xml-parser:parser", 61 | "//plugins/css/common", 62 | "@community//platform/core-api:core", 63 | "//plugins/css/psi", 64 | "@community//platform/core-ui", 65 | "@community//xml/impl", 66 | "//plugins/css/plugin", 67 | "//contrib/postcss", 68 | "//contrib/postcss:postcss_test_lib", 69 | "//plugins/JavaScriptLanguage:javascript-backend", 70 | "@community//platform/testFramework", 71 | "@community//platform/testFramework:testFramework_test_lib", 72 | "@community//xml/xml-psi-api:psi", 73 | "//plugins/css/common/psi", 74 | "//plugins/css/analysis", 75 | "//plugins/css/backend", 76 | "//plugins/JavaScriptLanguage/javascript-common", 77 | ] 78 | ) 79 | ### auto-generated section `build intellij.styled.components` end 80 | 81 | ### auto-generated section `test intellij.styled.components` start 82 | load("@community//build:tests-options.bzl", "jps_test") 83 | 84 | jps_test( 85 | name = "styled-components_test", 86 | runtime_deps = [":styled-components_test_lib"] 87 | ) 88 | ### auto-generated section `test intellij.styled.components` end -------------------------------------------------------------------------------- /src/test/com/intellij/styledComponents/CompletionTest.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.application.options.CodeStyle 4 | import com.intellij.codeInsight.lookup.Lookup 5 | import com.intellij.lang.javascript.formatter.JSCodeStyleSettings 6 | import com.intellij.psi.codeStyle.CodeStyleSettingsManager 7 | import com.intellij.testFramework.UsefulTestCase 8 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 9 | 10 | class CompletionTest : BasePlatformTestCase() { 11 | override fun getBasePath(): String = "plugins/styled-components/testData/completion" 12 | 13 | fun testCompletionAfterInterpolationExpressionInParentheses() { 14 | myFixture.configureByText("dummy.es6", 15 | "const HeroImage = styled(Box)`\n" + 16 | " position: relative;\n" + 17 | " &:after {\n" + 18 | " position: abs\${'out'}te;\n" + 19 | " background-image: url('\${p => p.bgSrc}');" + 20 | " border-radius: 6px;\n" + 21 | " color: \n" + 22 | " }\n" + 23 | "`") 24 | val lookupElements = myFixture.completeBasic().map { it.lookupString } 25 | assertContainsElements(lookupElements, "red", "blue") 26 | } 27 | 28 | fun testCssPropInJsx() { 29 | myFixture.configureByText("test.jsx", "
    />") 30 | val lookupElements = myFixture.completeBasic().map { it.lookupString } 31 | assertContainsElements(lookupElements, "css") 32 | } 33 | 34 | fun testCompleteCssPropWithQuotesForJSXAttributeSetting() { 35 | CodeStyle.doWithTemporarySettings(myFixture.project, CodeStyleSettingsManager.createTestSettings(null), Runnable { 36 | val jsCodeStyleSettings = CodeStyle.getSettings(myFixture.project).getCustomSettings(JSCodeStyleSettings::class.java) 37 | jsCodeStyleSettings.JSX_ATTRIBUTE_VALUE = JSCodeStyleSettings.JSXAttributeValuePresentation.TYPE_BASED 38 | myFixture.configureByText("test.jsx", "
    />") 39 | val cssItem = myFixture.completeBasic().find { it.lookupString == "css" } 40 | assertNotNull("expected 'css' item", cssItem) 41 | myFixture.lookup.currentItem = cssItem 42 | myFixture.finishLookup(Lookup.NORMAL_SELECT_CHAR) 43 | myFixture.checkResult("
    ") 44 | }) 45 | } 46 | 47 | fun testNoCssPropInHtml() { 48 | myFixture.configureByText("test.html", "
    />") 49 | val lookupElements = myFixture.completeBasic().map { it.lookupString } 50 | UsefulTestCase.assertDoesntContain(lookupElements, "css") 51 | } 52 | 53 | fun testAttrClassNames() { 54 | myFixture.copyFileToProject("styledComponentsStylesheet.css") 55 | doTest(arrayListOf("layout", "layout-wide", "layout-narrow")) 56 | } 57 | 58 | fun testAttrClassNamesNestedExpression() { 59 | myFixture.copyFileToProject("styledComponentsStylesheet.css") 60 | doTest(arrayListOf("layout-wide", "layout-narrow")) 61 | } 62 | 63 | fun testAttrClassNamesFunc() { 64 | myFixture.copyFileToProject("styledComponentsStylesheet.css") 65 | doTest(arrayListOf("col-xs", "col-sm")) 66 | } 67 | 68 | private fun doTest(expected: Collection, ext: String = ".js") { 69 | val testName = "${getTestName(true)}$ext" 70 | myFixture.configureByFile(testName) 71 | val lookupElements = myFixture.completeBasic()?.map { it.lookupString } ?: emptyList() 72 | assertContainsElements(lookupElements, expected) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/CssPropAttributeDescriptorProvider.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.javascript.nodejs.util.NodePackage 4 | import com.intellij.javascript.nodejs.util.NodePackageDescriptor 5 | import com.intellij.lang.javascript.DialectDetector 6 | import com.intellij.lang.javascript.frameworks.jsx.JSXAttributeDescriptorImpl 7 | import com.intellij.lang.javascript.psi.ecma6.impl.JSLocalImplicitElementImpl 8 | import com.intellij.lang.javascript.psi.types.JSNamedTypeFactory 9 | import com.intellij.lang.javascript.psi.types.JSTypeSource 10 | import com.intellij.openapi.application.ApplicationManager 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.roots.ProjectRootManager 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.openapi.vfs.VirtualFileManager 15 | import com.intellij.psi.util.CachedValueProvider 16 | import com.intellij.psi.util.CachedValuesManager 17 | import com.intellij.psi.xml.XmlTag 18 | import com.intellij.xml.XmlAttributeDescriptor 19 | import com.intellij.xml.XmlAttributeDescriptorsProvider 20 | 21 | private const val STYLED_COMPONENTS_PACKAGE_NAME = "styled-components" 22 | 23 | private val JS_STRING_TYPE = JSNamedTypeFactory.createStringPrimitiveType(JSTypeSource.EMPTY) 24 | 25 | private class CssPropAttributeDescriptorProvider : XmlAttributeDescriptorsProvider { 26 | override fun getAttributeDescriptors(tag: XmlTag?): Array { 27 | if (tag != null && isCssPropSupported(tag)) { 28 | return arrayOf(createCssPropertyDescriptor(tag)) 29 | } 30 | return emptyArray() 31 | } 32 | 33 | override fun getAttributeDescriptor(name: String?, tag: XmlTag?): XmlAttributeDescriptor? { 34 | if (tag != null && isCssPropSupported(tag) && name.equals("css")) { 35 | return createCssPropertyDescriptor(tag) 36 | } 37 | return null 38 | } 39 | 40 | private fun createCssPropertyDescriptor(tag: XmlTag): XmlAttributeDescriptor { 41 | val implicit = JSLocalImplicitElementImpl("css", JS_STRING_TYPE, tag, null) 42 | return JSXAttributeDescriptorImpl.create("css", implicit, JS_STRING_TYPE, true) 43 | } 44 | 45 | private fun isCssPropSupported(tag: XmlTag): Boolean { 46 | if (!DialectDetector.isJSX(tag)) { 47 | return false 48 | } 49 | val containingFile = tag.containingFile?.originalFile 50 | if (containingFile == null || containingFile.virtualFile == null) { 51 | return false 52 | } 53 | if (ApplicationManager.getApplication().isUnitTestMode) { 54 | return true 55 | } 56 | val virtualFile = containingFile.virtualFile 57 | val project = containingFile.project 58 | return CachedValuesManager.getManager(project).getCachedValue(containingFile) { 59 | val styledComponentsPackage = getNodePackage(STYLED_COMPONENTS_PACKAGE_NAME, project, virtualFile) 60 | val hasCssProp = styledComponentsPackage?.version?.isGreaterOrEqualThan(4, 0, 0) == true 61 | CachedValueProvider.Result(hasCssProp, VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS, 62 | ProjectRootManager.getInstance(project)) 63 | } 64 | } 65 | 66 | private fun getNodePackage(packageName: String, project: Project, virtualFile: VirtualFile): NodePackage? { 67 | val nodePackage = NodePackageDescriptor(packageName).findFirstDirectDependencyPackage(project, null, virtualFile) 68 | return if (nodePackage.isValid) nodePackage else null 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/StyledComponentsInjector.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.injection.MultiHostInjector 4 | import com.intellij.lang.injection.MultiHostRegistrar 5 | import com.intellij.lang.javascript.injections.JSFormattableInjectionUtil 6 | import com.intellij.lang.javascript.injections.StringInterpolationErrorFilter 7 | import com.intellij.lang.javascript.psi.JSExpression 8 | import com.intellij.lang.javascript.psi.JSLiteralExpression 9 | import com.intellij.patterns.PlatformPatterns 10 | import com.intellij.psi.PsiElement 11 | import com.intellij.psi.PsiLanguageInjectionHost 12 | import com.intellij.psi.xml.XmlAttributeValue 13 | import org.intellij.plugins.postcss.PostCssLanguage 14 | 15 | internal const val COMPONENT_PROPS_PREFIX = "div {" 16 | internal const val COMPONENT_PROPS_SUFFIX = "}" 17 | 18 | private class StyledComponentsInjector : MultiHostInjector { 19 | private object Holder { 20 | private val styledPattern = withNameStartingWith(listOf("styled")) 21 | private val builtinPlaces: List = listOf( 22 | PlaceInfo(taggedTemplate(PlatformPatterns.or(styledPattern, 23 | PlatformPatterns.psiElement(JSExpression::class.java) 24 | .withFirstChild(styledPattern))), COMPONENT_PROPS_PREFIX, COMPONENT_PROPS_SUFFIX), 25 | PlaceInfo(jsxAttribute("css"), COMPONENT_PROPS_PREFIX, COMPONENT_PROPS_SUFFIX), 26 | PlaceInfo(taggedTemplate(withReferenceName("extend")), COMPONENT_PROPS_PREFIX, COMPONENT_PROPS_SUFFIX), 27 | PlaceInfo(taggedTemplate(callExpression().withChild(withReferenceName("attrs"))), COMPONENT_PROPS_PREFIX, COMPONENT_PROPS_SUFFIX), 28 | PlaceInfo(taggedTemplate("css"), COMPONENT_PROPS_PREFIX, COMPONENT_PROPS_SUFFIX), 29 | PlaceInfo(taggedTemplate("injectGlobal")), 30 | PlaceInfo(taggedTemplate("createGlobalStyle")), 31 | PlaceInfo(taggedTemplate("keyframes"), "@keyframes foo {", "}"), 32 | PlaceInfo(jsxBodyText("style", "jsx")) 33 | ) 34 | 35 | fun matchInjectionTarget(injectionHost: PsiLanguageInjectionHost): PlaceInfo? { 36 | val customInjections = CustomInjectionsConfiguration.instance(injectionHost.project) 37 | return builtinPlaces.find { (elementPattern) -> elementPattern.accepts(injectionHost) } 38 | ?: customInjections.getInjectionPlaces().find { (elementPattern) -> elementPattern.accepts(injectionHost) } 39 | } 40 | } 41 | 42 | override fun elementsToInjectIn(): List> { 43 | return mutableListOf(JSLiteralExpression::class.java, XmlAttributeValue::class.java) 44 | } 45 | 46 | override fun getLanguagesToInject(registrar: MultiHostRegistrar, injectionHost: PsiElement) { 47 | if (injectionHost !is PsiLanguageInjectionHost) return 48 | 49 | val injectionLanguage = PostCssLanguage.INSTANCE 50 | val acceptedPattern = Holder.matchInjectionTarget(injectionHost) ?: return 51 | val stringPlaces = getInjectionPlaces(injectionHost) 52 | if (stringPlaces.isEmpty()) 53 | return 54 | 55 | registrar.startInjecting(injectionLanguage) 56 | stringPlaces.forEachIndexed { index, (prefix, range, suffix) -> 57 | val thePrefix = if (index == 0) acceptedPattern.prefix + prefix.orEmpty() else prefix 58 | val theSuffix = if (index == stringPlaces.size - 1) suffix.orEmpty() + acceptedPattern.suffix else suffix 59 | registrar.addPlace(thePrefix, theSuffix, injectionHost, range) 60 | } 61 | 62 | if (stringPlaces.size > 1) { 63 | StringInterpolationErrorFilter.register(registrar) 64 | StyledComponentsErrorFilter.register(registrar) 65 | } 66 | JSFormattableInjectionUtil.setReformattableInjection(injectionHost, registrar) 67 | registrar.doneInjecting() 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/Patterns.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.javascript.DialectDetector 4 | import com.intellij.lang.javascript.patterns.JSPatterns 5 | import com.intellij.lang.javascript.psi.* 6 | import com.intellij.lang.javascript.psi.ecma6.ES6TaggedTemplateExpression 7 | import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression 8 | import com.intellij.openapi.util.text.StringUtil 9 | import com.intellij.patterns.ElementPattern 10 | import com.intellij.patterns.PatternCondition 11 | import com.intellij.patterns.PlatformPatterns 12 | import com.intellij.patterns.XmlPatterns.* 13 | import com.intellij.psi.PsiElement 14 | import com.intellij.psi.util.PsiTreeUtil 15 | import com.intellij.psi.xml.XmlTag 16 | import com.intellij.psi.xml.XmlTokenType 17 | import com.intellij.util.ProcessingContext 18 | import com.intellij.util.SmartList 19 | import com.intellij.util.containers.ContainerUtil 20 | import java.util.* 21 | 22 | internal fun taggedTemplate(name: String): ElementPattern { 23 | return taggedTemplate(referenceExpression().withText(name)) 24 | } 25 | 26 | internal fun taggedTemplate(tagPattern: ElementPattern): ElementPattern { 27 | return PlatformPatterns.psiElement(JSStringTemplateExpression::class.java) 28 | .withParent(PlatformPatterns.psiElement(ES6TaggedTemplateExpression::class.java) 29 | .withChild(tagPattern)) 30 | } 31 | 32 | internal fun withReferenceName(name: String): ElementPattern { 33 | return referenceExpression() 34 | .with(object : PatternCondition("referenceName") { 35 | override fun accepts(referenceExpression: JSReferenceExpression, context: ProcessingContext): Boolean { 36 | return StringUtil.equals(referenceExpression.referenceName, name) 37 | } 38 | }) 39 | } 40 | 41 | internal fun referenceExpression() = PlatformPatterns.psiElement(JSReferenceExpression::class.java)!! 42 | internal fun callExpression() = PlatformPatterns.psiElement(JSCallExpression::class.java)!! 43 | 44 | internal fun withNameStartingWith(names: List): ElementPattern { 45 | return referenceExpression().with(object : PatternCondition("nameStartingWith") { 46 | override fun accepts(referenceExpression: JSReferenceExpression, context: ProcessingContext): Boolean { 47 | return ContainerUtil.startsWith(getReferenceParts(referenceExpression), names) 48 | } 49 | }) 50 | } 51 | 52 | internal fun getReferenceParts(jsReferenceExpression: JSReferenceExpression): List { 53 | val nameParts = SmartList() 54 | 55 | var ref: JSReferenceExpression? = jsReferenceExpression 56 | while (ref != null) { 57 | val name = ref.referenceName 58 | if (name.isNullOrBlank()) return ContainerUtil.emptyList() 59 | nameParts.add(name) 60 | val qualifier = ref.qualifier 61 | ref = qualifier as? JSReferenceExpression 62 | ?: PsiTreeUtil.findChildOfType(qualifier, JSReferenceExpression::class.java) 63 | } 64 | Collections.reverse(nameParts) 65 | return nameParts 66 | } 67 | 68 | fun jsxAttribute(name: String): ElementPattern { 69 | val cssAttributePattern = xmlAttributeValue(xmlAttribute(name) 70 | .withParent(xmlTag().with(object : PatternCondition("isJsx") { 71 | override fun accepts(tag: XmlTag, context: ProcessingContext): Boolean { 72 | return DialectDetector.isJSX(tag) 73 | } 74 | }))) 75 | 76 | //matches 'plain' attribute: '
    ' 77 | val stringValuedCssAttribute = cssAttributePattern 78 | .withChild(PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)) 79 | 80 | //matches JS literal inside JSX expression: '
    ' 81 | val jsInCssAttributePattern = JSPatterns.jsLiteralExpression() 82 | .with(object : PatternCondition("isStringLiteral") { 83 | override fun accepts(literal: JSLiteralExpression, context: ProcessingContext?): Boolean { 84 | return literal.isStringLiteral || literal is JSStringTemplateExpression 85 | } 86 | }).withAncestor(2, PlatformPatterns.psiElement(JSEmbeddedContent::class.java) 87 | .withParent(cssAttributePattern)) 88 | 89 | 90 | return PlatformPatterns.or(stringValuedCssAttribute, jsInCssAttributePattern) 91 | } 92 | 93 | fun jsxBodyText(tagName: String, vararg attributeNames: String): ElementPattern { 94 | return JSPatterns.jsLiteralExpression() 95 | .with(object : PatternCondition("isStringLiteral") { 96 | override fun accepts(literal: JSLiteralExpression, context: ProcessingContext?): Boolean { 97 | return literal.isStringLiteral || literal is JSStringTemplateExpression 98 | } 99 | }) 100 | .withAncestor(3, xmlTag().withName(tagName).withAnyAttribute(*attributeNames)) 101 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webstorm-styled-components 2 | 3 | ![Discord](https://img.shields.io/discord/818449605409767454?logo=discord) 4 | 5 | Support for styled-components 💅 in WebStorm 6 | 7 | The plugin can be installed in WebStorm, IntelliJ IDEA, PhpStorm, PyCharm Pro, and RubyMine v2017.2 and above. 8 | 9 | # Installation 10 | To install the plugin open the IDE `Preferences | Plugins`, then click `Browse repositories...` and search for `Styled Components`. 11 | 12 | # Features 13 | With this plugin you can enjoy the full coding assistance for styled-components 💅 14 | 15 | - Start typing to get code completion for CSS properties and values 16 | 17 | 18 | 19 | - Hit Alt-Enter to see available intentions and quick-fixes 20 | 21 | 22 | 23 | - Start typing in the interpolation to see completion suggestions for JavaScript variables, methods and functions 24 | 25 | 26 | 27 | - Cmd/Ctrl-click on the JavaScript symbol to go to its definition 28 | 29 | # Configuration 30 | To configure additional tags, search for 'styled-components' in the IDE preferences, and enter any additional tags to treat as styled components. 31 | 32 | For example, adding a value like `media` will enable CodeInsight for whose tag starts with it, e.g ``media.tablet`padding: 20px;` ``, ``media.desktop`padding: 10px;` ``. 33 | 34 | 35 | # FAQ 36 | - Why is code inside styled-components strings highlighted green? 37 | 38 | The IDE highlights injected language fragments by default. The highlighting can be disabled in `Preferences | Editor | Color Scheme | General | Inejcted Language Fragment`. 39 | - Why is code inside styled-components strings not reformatted? 40 | 41 | Formatting template strings with arguments is not currently supported by the IDE. Please follow this [IDE issue](https://youtrack.jetbrains.com/issue/WEB-28540) for updates. 42 | - Why am I seeing syntax errors after a template argument? 43 | 44 | 45 | The IDE's parser tries to determine what syntax element a template string argument replaces (property, value, etc). 46 | In some cases, it may be clear from code that at runtime the template argument will be a CSS property but not possible to infer the same statically: 47 | 48 | ```js 49 | const getColor = () => condition ? "color: red;" : "color: white;"; 50 | 51 | styled.div` 52 | ${getColor()} 53 | padding-right: 10px 54 | `; 55 | ``` 56 | In such cases, try placing a semicolon after the template argument: 57 | ```js 58 | styled.div` 59 | ${getColor()}; 60 | padding-right: 10px 61 | `; 62 | ``` 63 | 64 | # Contributing to the plugin 65 | Please report any issue with the plugin on [GitHub](https://github.com/styled-components/webstorm-styled-components/issues). We welcome your pull requests. 66 | 67 | The plugin is written in [Kotlin](https://kotlinlang.org/) and uses [Gradle](https://gradle.org/). 68 | 69 | **To start contributing** 70 | 1. Clone this repository. 71 | 2. Open the resulting directory in a recent version of Intellij IDEA (2017.*) using 'Open project'. 72 | 3. In the 'Import Project from Gradle' dialog, accept the default settings. 73 | 74 | * To **run tests** use `test` task (from the IDEA UI search for 'Execute Gradle Task' and select `test` or run `./gradlew test` from the command line) 75 | 76 | * To **launch IDEA** with the plugin built from your current sources use `runIde` 77 | 78 | * To **prepare a zip archive** for deployment use `buildPlugin` 79 | 80 | The project structure and dependencies are defined in [build.gradle](https://github.com/styled-components/webstorm-styled-components/blob/master/build.gradle). 81 | 82 | **Useful links** 83 | * [gradle-intellij-plugin](https://github.com/JetBrains/gradle-intellij-plugin) documentation on available Gradle tasks and build.gradle configuration options 84 | * [IntelliJ Platform SDK documentation](https://plugins.jetbrains.com/docs/intellij) describes IDE plugin structure in general 85 | * [Kotlin language reference](https://kotlinlang.org/docs/reference/) 86 | 87 | # License (MIT) 88 | Copyright 2017 Hossam Saraya 89 | 90 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 91 | 92 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 93 | 94 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 95 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /src/main/kotlin/com/intellij/styledComponents/StyledComponentsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.LanguageNamesValidation 4 | import com.intellij.lang.javascript.JavascriptLanguage 5 | import com.intellij.lang.javascript.refactoring.JSNamesValidation 6 | import com.intellij.openapi.editor.event.DocumentEvent 7 | import com.intellij.openapi.editor.event.DocumentListener 8 | import com.intellij.openapi.fileTypes.PlainTextLanguage 9 | import com.intellij.openapi.options.SearchableConfigurable 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.ui.ComponentValidator 12 | import com.intellij.openapi.ui.LabeledComponent 13 | import com.intellij.openapi.ui.ValidationInfo 14 | import com.intellij.openapi.util.Disposer 15 | import com.intellij.openapi.util.NlsContexts 16 | import com.intellij.ui.EditorTextField 17 | import com.intellij.ui.ToolbarDecorator 18 | import com.intellij.ui.table.JBTable 19 | import com.intellij.util.ui.EditableModel 20 | import com.intellij.util.ui.ItemRemovable 21 | import com.intellij.util.ui.table.* 22 | import java.awt.BorderLayout 23 | import javax.swing.JComponent 24 | import javax.swing.JPanel 25 | import javax.swing.JTable 26 | import javax.swing.table.AbstractTableModel 27 | 28 | private class StyledComponentsConfigurable(private val project: Project) : SearchableConfigurable { 29 | private val myConfiguration = CustomInjectionsConfiguration.instance(project) 30 | private val tagsModel = TagsModel() 31 | private val disposable = Disposer.newDisposable() 32 | 33 | private fun createPrefixesField() = object : JBListTable(JBTable(tagsModel), disposable) { 34 | override fun getRowRenderer(p0: Int): JBTableRowRenderer = 35 | object : EditorTextFieldJBTableRowRenderer(project, PlainTextLanguage.INSTANCE, disposable) { 36 | override fun getText(p0: JTable?, index: Int): String = tagsModel.myTags[index] 37 | } 38 | 39 | override fun getRowEditor(p0: Int): JBTableRowEditor = object : JBTableRowEditor() { 40 | override fun getValue(): JBTableRow = JBTableRow { (getComponent(0) as EditorTextField).text } 41 | override fun prepareEditor(p0: JTable?, p1: Int) { 42 | layout = BorderLayout() 43 | val editor = EditorTextField(tagsModel.myTags[p1]) 44 | editor.addDocumentListener(RowEditorChangeListener(0)) 45 | val validator = ComponentValidator(disposable) 46 | editor.addDocumentListener(object : DocumentListener { 47 | override fun documentChanged(event: DocumentEvent) { 48 | validator.updateInfo(getErrorText(editor.text)?.let { ValidationInfo(it, editor) }) 49 | } 50 | }) 51 | add(editor, BorderLayout.NORTH) 52 | validator.updateInfo(getErrorText(editor.text)?.let { ValidationInfo(it, editor) }) 53 | } 54 | 55 | override fun getFocusableComponents(): Array = arrayOf(preferredFocusedComponent) 56 | override fun getPreferredFocusedComponent(): JComponent = getComponent(0) as JComponent 57 | 58 | fun getErrorText(value: String?): @NlsContexts.DialogMessage String? { 59 | val trimmed = value?.trim() ?: "" 60 | val names = trimmed.split(".") 61 | if (trimmed.isBlank() || names.isEmpty()) { 62 | return StyledComponentsBundle.message("styled.components.configurable.error.value.is.empty") 63 | } 64 | return names.foldIndexed(null) { index, previous, string -> 65 | if (previous != null) { 66 | @Suppress("HardCodedStringLiteral") 67 | previous 68 | } 69 | else if (index == 0 && !LanguageNamesValidation.INSTANCE.forLanguage(JavascriptLanguage).isIdentifier(string, project)) { 70 | StyledComponentsBundle.message("styled.components.configurable.not.valid.identifier", string) 71 | } 72 | else if (!JSNamesValidation.isIdentifierName(string)) { 73 | StyledComponentsBundle.message("styled.components.configurable.not.valid.property.name", string) 74 | } 75 | else { 76 | null 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | override fun isModified(): Boolean { 84 | return !getPrefixesFromUi().contentEquals((myConfiguration.getTagPrefixes())) 85 | } 86 | 87 | private fun getPrefixesFromUi(): Array { 88 | return tagsModel.myTags.toTypedArray() 89 | } 90 | 91 | override fun getId(): String { 92 | return "styled-components" 93 | } 94 | 95 | override fun getDisplayName(): String { 96 | return StyledComponentsBundle.message("styled.components.configurable.title") 97 | } 98 | 99 | override fun apply() { 100 | myConfiguration.setTagPrefixes(getPrefixesFromUi()) 101 | com.intellij.util.FileContentUtil.reparseFiles(project, emptyList(), true) 102 | } 103 | 104 | override fun reset() { 105 | tagsModel.setTags(myConfiguration.getTagPrefixes()) 106 | } 107 | 108 | override fun createComponent(): JComponent { 109 | val tagPrefixesField = createPrefixesField() 110 | val table = ToolbarDecorator.createDecorator(tagPrefixesField.table).disableUpDownActions().createPanel() 111 | val component = LabeledComponent.create( 112 | table, StyledComponentsBundle.message("styled.components.configurable.label.tag.prefixes")) 113 | 114 | val panel = JPanel(BorderLayout()) 115 | panel.add(component, BorderLayout.NORTH) 116 | return panel 117 | } 118 | 119 | override fun disposeUIResources() { 120 | Disposer.dispose(disposable) 121 | } 122 | 123 | private class TagsModel : AbstractTableModel(), ItemRemovable, EditableModel { 124 | var myTags: MutableList = ArrayList() 125 | 126 | override fun getRowCount(): Int { 127 | return myTags.size 128 | } 129 | 130 | override fun getColumnCount(): Int { 131 | return 1 132 | } 133 | 134 | override fun getValueAt(row: Int, column: Int): Any = myTags[row] 135 | 136 | override fun setValueAt(o: Any?, row: Int, column: Int) { 137 | myTags[row] = o as String 138 | fireTableCellUpdated(row, column) 139 | } 140 | 141 | override fun addRow() { 142 | myTags.add("") 143 | val row = myTags.size - 1 144 | fireTableRowsInserted(row, row) 145 | } 146 | 147 | override fun exchangeRows(oldIndex: Int, newIndex: Int) {} 148 | 149 | override fun canExchangeRows(oldIndex: Int, newIndex: Int): Boolean { 150 | return false 151 | } 152 | 153 | override fun removeRow(idx: Int) { 154 | myTags.removeAt(idx) 155 | fireTableRowsDeleted(idx, idx) 156 | } 157 | 158 | fun setTags(tags: Array) { 159 | myTags.clear() 160 | myTags.addAll(tags) 161 | fireTableStructureChanged() 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/test/com/intellij/styledComponents/InjectionTest.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.styledComponents 2 | 3 | import com.intellij.lang.injection.InjectedLanguageManager 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.util.Disposer 6 | import com.intellij.psi.PsiElement 7 | import com.intellij.psi.PsiFile 8 | import com.intellij.psi.PsiLanguageInjectionHost 9 | import com.intellij.psi.util.PsiTreeUtil 10 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 11 | import com.intellij.util.containers.ContainerUtil 12 | import org.junit.Assert 13 | 14 | class InjectionTest : BasePlatformTestCase() { 15 | 16 | fun testTemplateArgumentIsWholeRange() { 17 | doTest("let css = css`\${someVariable}`") 18 | doTest("let globalCss = injectGlobal`\${someVariable}`") 19 | } 20 | 21 | fun testCss() { 22 | doTest("let css = css`\n" + 23 | " color:red;\n" + 24 | " width:100px;\n" + 25 | " height:100px;`", "div {\n" + 26 | " color:red;\n" + 27 | " width:100px;\n" + 28 | " height:100px;}") 29 | } 30 | 31 | fun testSimpleComponent() { 32 | doTest("const Title = styled.h1`\n" + 33 | " font-size: 1.5em;\n" + 34 | "`;", 35 | "div {\n" + 36 | " font-size: 1.5em;\n" + 37 | "}") 38 | } 39 | 40 | fun testComponentWithArgs() { 41 | doTest("const Button = styled.button`\n" + 42 | " /* Adapt the colours based on primary prop */\n" + 43 | " background: \${props => props.primary ? 'palevioletred' : 'white'};\n" + 44 | " color: \${props => props.primary ? 'white' : 'palevioletred'};\n" + 45 | " " + 46 | " font-size: 1em;\n" + 47 | "`;", "div {\n" + 48 | " /* Adapt the colours based on primary prop */\n" + 49 | " background: EXTERNAL_FRAGMENT_0;\n" + 50 | " color: EXTERNAL_FRAGMENT_1;\n" + 51 | " font-size: 1em;\n" + 52 | "}") 53 | } 54 | 55 | fun testComplexExpression() { 56 | doTest("const Input = styled.input.attrs({\n" + 57 | " type: 'password',\n" + 58 | "\n" + 59 | " // or we can define dynamic ones\n" + 60 | " margin: props => props.size || '1em',\n" + 61 | " padding: props => props.size || '1em',\n" + 62 | "})`\n" + 63 | " color: palevioletred;\n" + 64 | "`;", "div {\n" + 65 | " color: palevioletred;\n" + 66 | "}") 67 | } 68 | 69 | fun testComplexExpression2() { 70 | doTest("const ContactMenuIcon = ((styled(Icon)))" + 71 | ".attrs({ iconName: 'contact_card' })`\n" + 72 | " line-height: 0;\n" + 73 | "`", "div {\n" + 74 | " line-height: 0;\n" + 75 | "}") 76 | } 77 | 78 | fun testKeyframes() { 79 | doTest("const rotate360 = keyframes`\n" + 80 | " from{\n" + 81 | " transform:rotate(0deg);\n" + 82 | " }\n" + 83 | "\n" + 84 | " to{\n" + 85 | " transform:rotate(360deg);\n" + 86 | " }\n" + 87 | "`;", 88 | "@keyframes foo {\n" + 89 | " from{\n" + 90 | " transform:rotate(0deg);\n" + 91 | " }\n" + 92 | "\n" + 93 | " to{\n" + 94 | " transform:rotate(360deg);\n" + 95 | " }\n" + 96 | "}") 97 | 98 | } 99 | 100 | fun testExtendsComponent() { 101 | doTest("const TomatoButton = Button.extend`\n" + 102 | " color: tomato;\n" + 103 | " border-color: tomato;\n" + 104 | "`;", 105 | "div {\n" + 106 | " color: tomato;\n" + 107 | " border-color: tomato;\n" + 108 | "}") 109 | } 110 | 111 | fun testComponentAttrs() { 112 | doTest("const div = styled.div;\n" + 113 | "const FilterIcon = div.attrs({ iconName: 'filter' })`\n" + 114 | " line-height: 0;\n" + 115 | "`", "div {\n" + 116 | " line-height: 0;\n" + 117 | "}") 118 | } 119 | 120 | fun testInjectGlobal() { 121 | doTest("injectGlobal`\n" + 122 | " div{\n" + 123 | " color:red\n" + 124 | " }\n" + 125 | "`", "\n" + 126 | " div{\n" + 127 | " color:red\n" + 128 | " }\n") 129 | } 130 | 131 | fun testTemplateArgsAtStartEndOfString() { 132 | doTest("let atStart = styled.div`\${getPropName()}:red`\n" + 133 | "let atEnd = styled.div`color:\${getColor()}`\n", 134 | "div {EXTERNAL_FRAGMENT_0:red}", 135 | "div {color:EXTERNAL_FRAGMENT_0}") 136 | } 137 | 138 | fun testWithCustomInjectionMediaQuery() { 139 | setCustomInjectionsConfiguration("media") 140 | doTest("const Container = styled.div`\n" + 141 | " color: #333;\n" + 142 | " \${media.desktop `padding: 0 20px;` }\n" + 143 | "`", "div {\n" + 144 | " color: #333;\n" + 145 | " EXTERNAL_FRAGMENT_0\n" + 146 | "}", "div {padding: 0 20px;}") 147 | } 148 | 149 | fun testWithUnqualifiedCustomTag() { 150 | setCustomInjectionsConfiguration("sc") 151 | doTest("const Container = sc`color: #333;`;", "div {color: #333;}") 152 | } 153 | 154 | fun testCustomInjectionWithComplexTag() { 155 | setCustomInjectionsConfiguration("bp") 156 | doTest("const Container = styled.div`\n" + 157 | " color: #333;\n" + 158 | " \${bp(media.tablet)`padding: 0 20px;` }\n" + 159 | "`", "div {\n" + 160 | " color: #333;\n" + 161 | " EXTERNAL_FRAGMENT_0\n" + 162 | "}", "div {padding: 0 20px;}") 163 | } 164 | 165 | fun testCssProperty_DoubleQuotedAttributeValue() { 166 | doTest("
    ", "div {color:red}") 167 | } 168 | 169 | fun testCssProperty_SingleQuotedAttributeValue() { 170 | doTest("
    ", "div {color:red}") 171 | } 172 | 173 | fun testCssProperty_TemplateStringInValue() { 174 | doTest("
    ", "div {color:red}") 175 | } 176 | 177 | fun testCssProperty_PlainJSStringInValue() { 178 | doTest("
    ", "div {color:red}") 179 | } 180 | 181 | fun testNoCssPropertyInjectionInHtml() { 182 | doTestWithExtension("
    ", "html", emptyArray()) 183 | } 184 | 185 | fun testNoInjectionWithObjectInCssProperty() { 186 | doTest("
    ") 187 | } 188 | 189 | fun testStyledJsx() { 190 | doTest("", "\n" + 196 | " .container {\n" + 197 | " margin: 0 auto;\n" + 198 | " width: 880px\n" + 199 | " }\n") 200 | } 201 | 202 | fun testArgumentNestedInjectionBeforeProperty() { 203 | doTest("const ErrorDiv = styled.div`\n" + 204 | " \${props =>\n" + 205 | " css`\n" + 206 | " color: red; \n" + 207 | " `}\n" + 208 | " color: blue; \n" + 209 | "`;", "div {\n" + 210 | " EXTERNAL_FRAGMENT_0\n" + 211 | " color: blue; \n" + 212 | "}", "div {\n" + 213 | " color: red; \n" + 214 | " }" 215 | ) 216 | } 217 | 218 | fun testArgumentNestedInjectionAfterProperty() { 219 | doTest("const ErrorDiv = styled.div`\n" + 220 | " color: blue;\n" + 221 | " \${props =>\n" + 222 | " css`\n" + 223 | " color: red;\n" + 224 | " `}\n" + 225 | "`;", "div {\n" + 226 | " color: blue;\n" + 227 | " EXTERNAL_FRAGMENT_0\n" + 228 | "}", "div {\n" + 229 | " color: red;\n" + 230 | " }" 231 | ) 232 | } 233 | 234 | fun testArgumentNestedInjectionOnlyArgument() { 235 | doTest("const OptionLabel = styled.div`\n" + 236 | " \${(props) => css`\n" + 237 | " margin-bottom: 0.3em;\n" + 238 | " `}\n" + 239 | "`;", "div {\n" + 240 | " EXTERNAL_FRAGMENT_0\n" + 241 | "}", "div {\n" + 242 | " margin-bottom: 0.3em;\n" + 243 | " }" 244 | ) 245 | } 246 | 247 | fun testArgumentNestedInjectionLeadingArgument() { 248 | doTest("const OptionLabel = styled.div`\${(props) => css`margin-bottom: 0.3em;`} `;", 249 | "div {EXTERNAL_FRAGMENT_0 }", 250 | "div {margin-bottom: 0.3em;}" 251 | ) 252 | } 253 | 254 | fun testArgumentNestedInjectionTrailingArgument() { 255 | doTest("const OptionLabel = styled.div` \${(props) => css`margin-bottom: 0.3em;`}`;", 256 | "div { EXTERNAL_FRAGMENT_0}", 257 | "div {margin-bottom: 0.3em;}" 258 | ) 259 | } 260 | 261 | fun testArgumentNestedInjectionLeadingAndTrailingArgument() { 262 | doTest("const OptionLabel = styled.div`\${(props) => css`margin-bottom: 0.3em;`} padding: \${(props) => `5px;`}`;", 263 | "div {EXTERNAL_FRAGMENT_0 padding: EXTERNAL_FRAGMENT_1}", 264 | "div {margin-bottom: 0.3em;}" 265 | ) 266 | } 267 | 268 | fun testArgumentNestedInjectionAdjacentArguments() { 269 | doTest("const OptionLabel = styled.div`padding: 3px; " + 270 | "\${(props) => css`margin-bottom: 0.3em;`}\${'BETWEEN-'}\${props => 'display'}: none;`;", 271 | "div {padding: 3px; EXTERNAL_FRAGMENT_0BETWEEN-EXTERNAL_FRAGMENT_2: none;}", 272 | "div {margin-bottom: 0.3em;}" 273 | ) 274 | } 275 | 276 | fun testArgumentNestedInjectionAdjacentArgumentsLeading() { 277 | doTest("const OptionLabel = styled.div`" + 278 | "\${(props) => css`margin-bottom: 0.3em;`}\${'margin'}: 20px;\${props => 'display'}: none;`;", 279 | "div {EXTERNAL_FRAGMENT_0margin: 20px;EXTERNAL_FRAGMENT_2: none;}", 280 | "div {margin-bottom: 0.3em;}" 281 | ) 282 | } 283 | 284 | fun testArgumentNestedInjectionAdjacentArgumentsTrailing() { 285 | doTest("const OptionLabel = styled.div`padding: 3px; " + 286 | "\${(props) => css`margin-bottom: 0.3em;`}\${'BETWEEN-'}\${props => 'display'}: none; \${'background: red'}`;", 287 | "div {padding: 3px; EXTERNAL_FRAGMENT_0BETWEEN-EXTERNAL_FRAGMENT_2: none; background: red}", 288 | "div {margin-bottom: 0.3em;}" 289 | ) 290 | } 291 | 292 | fun testArgumentNestedInjectionAdjacentArgumentsWithInjectionInBetween() { 293 | doTest("const OptionLabel = styled.div`padding: 3px; " + 294 | "\${(props) => css`margin-bottom: 0.3em;`}\${(props) => css`margin-top: 0.3em;`}\${props => 'display'}: none;`;", 295 | "div {padding: 3px; EXTERNAL_FRAGMENT_0EXTERNAL_FRAGMENT_1EXTERNAL_FRAGMENT_2: none;}", 296 | "div {margin-bottom: 0.3em;}", 297 | "div {margin-top: 0.3em;}" 298 | ) 299 | } 300 | 301 | fun testArgumentNestedPlainStringCss() { 302 | doTest("const ErrorDiv = styled.div`\n" + 303 | " \${props =>\n" + 304 | " `\n" + 305 | " color: red; \n" + 306 | " `};\n" + 307 | " color: blue; \n" + 308 | "`;", "div {\n" + 309 | " EXTERNAL_FRAGMENT_0;\n" + 310 | " color: blue; \n" + 311 | "}" 312 | ) 313 | } 314 | 315 | fun testArgumentsInlinedToInjection() { 316 | doTest("styled.div`\${false}: absolute;\${reference}: none;`", 317 | "div {false: absolute;reference: none;}" 318 | ) 319 | } 320 | 321 | private fun setCustomInjectionsConfiguration(vararg prefixes: String) { 322 | val configuration = CustomInjectionsConfiguration.instance(myFixture.project) 323 | val previousPrefixes = configuration.getTagPrefixes() 324 | configuration.setTagPrefixes(arrayOf(*prefixes)) 325 | Disposer.register(myFixture.testRootDisposable, Disposable { configuration.setTagPrefixes(previousPrefixes) }) 326 | } 327 | 328 | private fun doTest(fileContent: String, vararg expected: String) { 329 | doTestWithExtension(fileContent, "jsx", expected) 330 | } 331 | 332 | private fun doTestWithExtension(fileContent: String, extension: String, expected: Array) { 333 | myFixture.setCaresAboutInjection(false) 334 | val file = myFixture.configureByText("dummy.$extension", fileContent) 335 | myFixture.testHighlighting(true, false, false) 336 | Assert.assertEquals(expected.toList(), collectInjectedPsiContents(file)) 337 | } 338 | 339 | private fun collectInjectedPsiContents(file: PsiFile): List { 340 | return ContainerUtil.map(collectInjectedPsiFiles(file)) { element -> element.text } 341 | } 342 | 343 | private fun collectInjectedPsiFiles(file: PsiFile): List { 344 | val result = LinkedHashSet() 345 | PsiTreeUtil.processElements(file) { 346 | val host = it as? PsiLanguageInjectionHost 347 | if (host != null) { 348 | InjectedLanguageManager.getInstance(host.project) 349 | .enumerate(host) { injectedPsi, _ -> result.add(injectedPsi) } 350 | } 351 | true 352 | } 353 | 354 | return ArrayList(result) 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------