Skip to content

Commit 0d02725

Browse files
committed
fix: support css import completion recursively
1 parent 6a011d8 commit 0d02725

12 files changed

Lines changed: 171 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
## [Unreleased]
44

5+
## [1.7.1] - 2026-04-26
6+
7+
### Bug Fixes
8+
9+
- fix: restore completion stability by removing runtime linkage to Sass/SCSS PSI classes
10+
- fix: resolve selectors from CSS `@import` chains without binding to version-specific `CssImport.resolve()` ABI
11+
12+
### Tests
13+
14+
- test: add completion coverage for CSS `@import` and circular `@import` chains
15+
16+
### Docs
17+
18+
- docs: align README, plugin metadata, and CLAUDE.md with the current CSS-only feature scope
19+
520
## [1.7.0] - 2026-04-26
621

722
### Bug Fixes
@@ -184,7 +199,9 @@
184199
- support a little complex parents selector
185200
- support css selector has pseudo
186201

187-
[Unreleased]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.6.0...HEAD
202+
[Unreleased]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.7.1...HEAD
203+
[1.7.1]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.7.0...v1.7.1
204+
[1.7.0]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.6.0...v1.7.0
188205
[1.6.0]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.4...v1.6.0
189206
[1.5.4]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.3...v1.5.4
190207
[1.5.3]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.2...v1.5.3

CLAUDE.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,14 @@ This is an IntelliJ Platform plugin that provides CSS Modules intelligence for J
3737
- `IndexAccessCompletionProvider` for bracket syntax (`styles['...']`) — matches literal inside indexed access
3838
- `DotAccessCompletionProvider` for dot syntax (`styles.className`) — matches reference expressions preceded by `.`; auto-converts hyphenated names to bracket syntax on insertion
3939

40-
### Annotation & quick fix (warnings on unknown classes)
41-
**Entry:** `CssModulesClassAnnotator` — for both bracket and dot syntax, checks if the referenced class exists in the stylesheet. If not, shows a warning with `SimpleCssSelectorFix` which creates the missing CSS ruleset and navigates the editor to it.
42-
4340
### Hover documentation
4441
**Entry:** `SimpleDocumentationProvider` — renders the CSS ruleset content when hovering over a class name in JS/TS.
4542

4643
### Core utility
4744

4845
`src/main/kotlin/com/peppa/css/completion/QCssModulesUtil.kt` is the central utility file:
4946

50-
- **`restoreAllSelector(stylesheetFile)`** — parses a stylesheet and returns a map of class name → pointer to CSS ruleset. Handles `&` parent selectors (SCSS/LESS) via `processAmpersandSelectors`, plain class selectors via `processAllSelectorSuffixes`, and recursively resolves `@import`/`@use` statements (with partial/underscore naming conventions and `~` node-style resolution).
47+
- **`restoreAllSelector(stylesheetFile)`** — parses a stylesheet and returns a map of class name → pointer to CSS ruleset. Handles nested selector expansion via `processAmpersandSelectors`, plain class selectors via `processAllSelectorSuffixes`, and recursively resolves CSS `@import` statements.
5148
- **`findReferenceStyleFile(element)`** — resolves a JS expression back to the `StylesheetFile` it imports (handles direct imports, default imports, re-exports).
5249
- **`generateLookupElementList(stylesheetFile)`** — builds completion `LookupElement`s from the parsed selector map.
5350

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
![Rating](https://img.shields.io/jetbrains/plugin/r/rating/react.css.module.all)
55
![Version](https://img.shields.io/jetbrains/plugin/v/react.css.module.all)
66

7-
IntelliJ/WebStorm plugin for CSS Modules in JavaScript and TypeScript. Provides code completion, go-to-definition navigation, quick documentation, and missing-class quick fixes for `import styles from './file.css'`.
7+
IntelliJ/WebStorm plugin for CSS Modules in JavaScript and TypeScript. Provides code completion, go-to-definition navigation, and quick documentation for `import styles from './file.css'`.
88

99
## Features
1010

1111
- **Completion** — class name completion for bracket syntax (`styles['...']`) and dot syntax (`styles....`); hyphenated names auto-convert to bracket syntax on insertion
1212
- **Navigation** — Ctrl+Click / Go to Definition on a class name jumps to the CSS ruleset
1313
- **Quick documentation** — hover over a class name to see the corresponding CSS block
14-
- **Annotations** — unknown class names are highlighted with a quick fix to create the missing ruleset
15-
- **Sass/SCSS/LESS** — resolves parent selectors (`&`), `@import`, `@use`, and `@forward`
16-
- **Circular import safety** — gracefully short-circuits circular `@import`/`@use` chains
14+
- **CSS imports** — resolves selectors from CSS `@import` chains
15+
- **Circular import safety** — gracefully short-circuits circular CSS `@import` chains
1716

1817
## Install
1918

build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ kotlin {
1818

1919

2020
repositories {
21+
maven(url = uri("/home/demo/jetbrains-mirror/local-maven"))
22+
maven(url = "https://maven.aliyun.com/repository/public")
23+
maven(url = "https://maven.aliyun.com/repository/central")
2124
mavenCentral()
2225
intellijPlatform {
2326
defaultRepositories()
@@ -31,11 +34,16 @@ dependencies {
3134
intellijPlatform {
3235
webstorm("2024.2")
3336
bundledPlugin("JavaScript")
34-
bundledPlugin("org.jetbrains.plugins.sass")
3537
testFramework(TestFrameworkType.Platform)
3638
}
3739
}
3840

41+
tasks.test {
42+
testLogging {
43+
showStandardStreams = true
44+
}
45+
}
46+
3947
intellijPlatform {
4048
pluginConfiguration {
4149
version = providers.gradleProperty("version")

gradle.properties

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ pluginGroup = com.peppa.css
44
pluginName = CSS Modules
55
pluginRepositoryUrl = https://github.com/Q-Peppa/react-css-modules-all
66
# SemVer format -> https://semver.org
7-
pluginVersion = 1.7.0
8-
version = 1.7.0
7+
pluginVersion = 1.7.1
8+
version = 1.7.1
99
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
1010
pluginSinceBuild=242
1111
# pluginUntilBuild = 242.* # for Supported all version > 231
@@ -20,4 +20,7 @@ kotlin.stdlib.default.dependency = false
2020
org.gradle.configuration-cache = true
2121

2222
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
23-
org.gradle.caching = true
23+
org.gradle.caching = true
24+
25+
# Prefer direct JetBrains repository URLs over Cache Redirector in restricted networks.
26+
useCacheRedirector = false

src/main/kotlin/com/peppa/css/completion/QCssModulesUtil.kt

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,67 @@ fun restoreAllSelector(stylesheetFile: StylesheetFile): Map<String, SmartPsiElem
4747

4848
return CachedValuesManager.getCachedValue(stylesheetFile) {
4949
val result = linkedMapOf<String, SmartPsiElementPointer<PsiElement>>()
50-
val scope = GlobalSearchScope.fileScope(stylesheetFile.project, stylesheetFile.virtualFile)
51-
52-
// Resolve parent selector (&) for SCSS/LESS nesting
53-
CssIndexUtil.processAmpersandSelectors(stylesheetFile.project, scope) { selector ->
54-
selector.processAmpersandEvaluatedSelectors { evaluated ->
55-
recessivesClassInCssSelector(selector, evaluated, result, pointerManager)
56-
}
57-
true
58-
}
50+
val dependencies = linkedSetOf<PsiElement>()
51+
val visitedFiles = linkedSetOf<StylesheetFile>()
52+
collectSelectorsRecursively(stylesheetFile, result, pointerManager, visitedFiles, dependencies)
5953

60-
// Resolve all class selectors
61-
CssIndexUtil.processAllSelectorSuffixes(
62-
CssSelectorSuffixType.CLASS,
63-
stylesheetFile.project,
64-
scope
65-
) { name, css ->
66-
css.ruleset?.let { result[name] = pointerManager.createSmartPsiElementPointer(it) }
67-
true
54+
CachedValueProvider.Result.create(result.toMap(), *dependencies.toTypedArray())
55+
}
56+
}
57+
58+
private fun collectSelectorsRecursively(
59+
stylesheetFile: StylesheetFile,
60+
result: MutableMap<String, SmartPsiElementPointer<PsiElement>>,
61+
pointerManager: SmartPointerManager,
62+
visitedFiles: MutableSet<StylesheetFile>,
63+
dependencies: MutableSet<PsiElement>
64+
) {
65+
if (!visitedFiles.add(stylesheetFile)) return
66+
dependencies += stylesheetFile
67+
68+
val scope = GlobalSearchScope.fileScope(stylesheetFile.project, stylesheetFile.virtualFile)
69+
70+
CssIndexUtil.processAmpersandSelectors(stylesheetFile.project, scope) { selector ->
71+
selector.processAmpersandEvaluatedSelectors { evaluated ->
72+
recessivesClassInCssSelector(selector, evaluated, result, pointerManager)
6873
}
74+
true
75+
}
6976

70-
CachedValueProvider.Result.create(result.toMap(), stylesheetFile)
77+
CssIndexUtil.processAllSelectorSuffixes(
78+
CssSelectorSuffixType.CLASS,
79+
stylesheetFile.project,
80+
scope
81+
) { name, css ->
82+
css.ruleset?.let { result[name] = pointerManager.createSmartPsiElementPointer(it) }
83+
true
7184
}
85+
86+
findImportedStylesheetFiles(stylesheetFile).forEach { importedFile ->
87+
collectSelectorsRecursively(importedFile, result, pointerManager, visitedFiles, dependencies)
88+
}
89+
}
90+
91+
private fun findImportedStylesheetFiles(stylesheetFile: StylesheetFile): Sequence<StylesheetFile> {
92+
return PsiTreeUtil.findChildrenOfType(stylesheetFile, CssImport::class.java).asSequence()
93+
.flatMap { resolveCssImportTargets(it) }
94+
.mapNotNull { it as? StylesheetFile }
95+
}
96+
97+
private fun resolveCssImportTargets(cssImport: CssImport): Sequence<PsiFile> {
98+
val resolvedByMethod = runCatching {
99+
CssImport::class.java.getMethod("resolve").invoke(cssImport) as? Array<*>
100+
}.getOrNull()
101+
?.asSequence()
102+
?.mapNotNull { it as? PsiFile }
103+
104+
if (resolvedByMethod != null) {
105+
return resolvedByMethod
106+
}
107+
108+
return cssImport.uriElements.asSequence()
109+
.flatMap { uriElement -> uriElement.references.asSequence() }
110+
.mapNotNull { it.resolve() as? PsiFile }
72111
}
73112

74113

src/main/resources/META-INF/plugin.xml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
<li><b>Smart Completion</b> - Auto-complete CSS class names from imported style modules</li>
1414
<li><b>Dual Syntax Support</b> - Works with both <code>styles['className']</code> and <code>styles.className</code></li>
1515
<li><b>Go to Definition</b> - Click on class name to navigate directly to CSS/SCSS definition</li>
16-
<li><b>Unknown Class Detection</b> - Highlights undefined CSS classes with warnings</li>
17-
<li><b>Quick Fix</b> - Instantly create missing CSS selectors with one click</li>
18-
<li><b>Parent Selector Support</b> - Full support for SCSS/LESS <code>&amp;</code> (parent selector) syntax</li>
16+
<li><b>CSS Import Support</b> - Follows CSS <code>@import</code> chains for completion and navigation</li>
1917
<li><b>Hover Documentation</b> - Preview CSS rules on mouse hover</li>
2018
</ul>
2119
2220
<h3>Supported File Types</h3>
2321
<ul>
24-
<li>CSS, SCSS, LESS modules</li>
22+
<li>CSS modules</li>
2523
<li>JavaScript, TypeScript, JSX, TSX</li>
2624
</ul>
2725
]]>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.peppa.css.css
2+
3+
import com.intellij.codeInsight.completion.CompletionType
4+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
5+
6+
class ImportedStylesheetCompletionTest : BasePlatformTestCase() {
7+
8+
override fun getTestDataPath() = "src/test/resources"
9+
10+
private fun assertAndPrintCompletion(expected: List<String>) {
11+
val actual = myFixture.lookupElementStrings
12+
println("actual completion strings: ${actual?.sorted()}")
13+
println("expected completion strings: ${expected.sorted()}")
14+
assertNotNull(actual)
15+
assertContainsElements(actual!!, *expected.toTypedArray())
16+
}
17+
18+
fun testCssImportCompletionIncludesImportedSelectors() {
19+
myFixture.copyFileToProject("imports/main.css")
20+
myFixture.copyFileToProject("imports/imported.css")
21+
22+
myFixture.configureByText(
23+
"test.js", """
24+
import s from "./imports/main.css";
25+
s['<caret>'];
26+
""".trimIndent()
27+
)
28+
29+
myFixture.complete(CompletionType.BASIC)
30+
31+
assertAndPrintCompletion(listOf("localCard", "sharedCard", "importedCard"))
32+
}
33+
34+
fun testCircularCssImportsDoNotLoopAndStillResolveSelectors() {
35+
myFixture.copyFileToProject("imports/cycle-a.css")
36+
myFixture.copyFileToProject("imports/cycle-b.css")
37+
38+
myFixture.configureByText(
39+
"test.js", """
40+
import s from "./imports/cycle-a.css";
41+
s['<caret>'];
42+
""".trimIndent()
43+
)
44+
45+
myFixture.complete(CompletionType.BASIC)
46+
47+
assertAndPrintCompletion(listOf("cycleA", "cycleB"))
48+
}
49+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@import "./cycle-b.css";
2+
3+
.cycleA {
4+
color: black;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@import "./cycle-a.css";
2+
3+
.cycleB {
4+
color: white;
5+
}

0 commit comments

Comments
 (0)