Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.DS_Store
.gradle
.idea
.intellijPlatform
.kotlin
.qodana
build
.kotlin
build
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased]

## [1.5.4] - 2025-12-10

- 升级版本号到 1.5.4 并准备发布
- improve CSS Modules class reference resolution and caching mechanism

## [1.5.3] - 2025-12-10

- 将插件名称从 "React Css Modules All" 更改为 "CSS Modules"
Expand Down Expand Up @@ -155,7 +160,8 @@
- support a little complex parents selector
- support css selector has pseudo

[Unreleased]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.3...HEAD
[Unreleased]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.4...HEAD
[1.5.4]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.3...v1.5.4
[1.5.3]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.2...v1.5.3
[1.5.2]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.1...v1.5.2
[1.5.1]: https://github.com/Q-Peppa/react-css-modules-all/compare/v1.5.0...v1.5.1
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ case it will be transformed css name case to camelCase

### SnapShot

![1.jpg](src%2Fmain%2Fresources%2Fpic%2F1.jpg)
![2.jpg](src%2Fmain%2Fresources%2Fpic%2F2.jpg)
![3.jpg](src%2Fmain%2Fresources%2Fpic%2F3.jpg)
![4.jpg](src%2Fmain%2Fresources%2Fpic%2F4.png)
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.kotlin) // Kotlin support
alias(libs.plugins.changelog) // Gradle Changelog Plugin
alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin'
alias(libs.plugins.qodana) // Gradle Qodana Plugin
}


Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pluginGroup = com.peppa.css
pluginName = CSS Modules
pluginRepositoryUrl = https://github.com/Q-Peppa/react-css-modules-all
# SemVer format -> https://semver.org
pluginVersion = 1.5.3
version = 1.5.3
pluginVersion = 1.5.4
version = 1.5.4
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild=242
# pluginUntilBuild = 242.* # for Supported all version > 231
Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ opentest4j = "1.3.0"
changelog = "2.5.0"
intelliJPlatform = "2.10.5"
kotlin = "2.2.21"
qodana = "2025.2.2"

[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
Expand All @@ -15,4 +16,5 @@ opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "ope
[plugins]
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" }
12 changes: 12 additions & 0 deletions qodana.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Qodana configuration:
# https://www.jetbrains.com/help/qodana/qodana-yaml.html

version: "1.0"
linter: jetbrains/qodana-jvm-community:2024.3
projectJDK: "21"
profile:
name: qodana.recommended
exclude:
- name: All
paths:
- .qodana
72 changes: 27 additions & 45 deletions src/main/kotlin/com/peppa/css/annotator/CssModulesClassAnnotator.kt
Original file line number Diff line number Diff line change
@@ -1,67 +1,49 @@
package com.peppa.css.annotator

import com.peppa.css.completion.resolveStylesheetFromReference
import com.peppa.css.psi.CssModuleClassReference
import com.peppa.css.psi.isStyleIndex
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.Annotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.lang.javascript.psi.JSLiteralExpression
import com.intellij.lang.javascript.psi.JSReferenceExpression
import com.intellij.psi.PsiElement
import com.intellij.psi.css.CssRuleset
import org.jetbrains.annotations.NotNull
import java.util.Objects

import com.peppa.css.completion.resolveStylesheetFromReference
import com.peppa.css.psi.CssModuleClassReference
import com.peppa.css.psi.isStyleIndex

const val MESSAGE = "Selector declarations is Empty"
const val UNKNOWN = "Unknown class name"
private const val MESSAGE_UNKNOWN = "Unknown class name"

class CssModulesClassAnnotator : Annotator {
private fun resolveUnknownClass(holder: AnnotationHolder, psiElement: JSLiteralExpression) {
val cssSelectorName = psiElement.stringValue?.trim().orEmpty()
val reference = psiElement.reference

// Check if the reference is unresolved (CSS class doesn't exist)
if (reference is CssModuleClassReference && reference.isUnresolved()) {
val message = "$UNKNOWN \"$cssSelectorName\""
holder.newAnnotation(HighlightSeverity.WARNING, message)
.range(psiElement)
.withFix(SimpleCssSelectorFix(cssSelectorName, reference.stylesheetFile))
.create()
}
}
override fun annotate(psiElement: PsiElement, holder: AnnotationHolder) {
when (psiElement) {
is JSLiteralExpression if isStyleIndex(psiElement) ->
annotateStyleIndex(psiElement, holder)

private fun resolveEmptyClass(holder: AnnotationHolder, psiElement: JSLiteralExpression) {
val ruleset = psiElement.reference?.resolve()
if (ruleset is CssRuleset) {
val declarations = ruleset.block?.declarations
if (declarations.isNullOrEmpty()) {
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, MESSAGE)
.range(psiElement)
.create()
}
is JSReferenceExpression -> annotateUnresolvedReference(psiElement, holder)
}
}

override fun annotate(@NotNull psiElement: PsiElement, @NotNull holder: AnnotationHolder) {
if (psiElement is JSLiteralExpression && isStyleIndex(psiElement)) {
resolveUnknownClass(holder, psiElement)
resolveEmptyClass(holder, psiElement)
return
private fun annotateStyleIndex(element: JSLiteralExpression, holder: AnnotationHolder) {
val reference = element.reference as? CssModuleClassReference ?: return
val className = element.stringValue?.trim().orEmpty()

when {
reference.isUnresolved() -> holder.newAnnotation(HighlightSeverity.WARNING, "$MESSAGE_UNKNOWN \"$className\"")
.range(element)
.withFix(SimpleCssSelectorFix(className, reference.stylesheetFile))
.create()
}
if (psiElement is JSReferenceExpression && Objects.isNull(psiElement.reference?.resolve())) {
val styleFile = resolveStylesheetFromReference(psiElement) ?: return
}

private fun annotateUnresolvedReference(element: JSReferenceExpression, holder: AnnotationHolder) {
if (element.reference?.resolve() != null) return

holder.newAnnotation(
HighlightSeverity.WEAK_WARNING,
"$UNKNOWN \"${psiElement.lastChild.text}\""
)
.range(psiElement.lastChild)
.withFix(SimpleCssSelectorFix(psiElement.lastChild.text, styleFile))
.create()
val styleFile = resolveStylesheetFromReference(element) ?: return
val className = element.lastChild.text

}
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, "$MESSAGE_UNKNOWN \"$className\"")
.range(element.lastChild)
.withFix(SimpleCssSelectorFix(className, styleFile))
.create()
}
}
20 changes: 11 additions & 9 deletions src/main/kotlin/com/peppa/css/annotator/SimpleCssSelectorFix.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,26 @@

override fun invoke(@NotNull project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file == null) return
val rulesetText = "\n.$key {\n \n}"

val rulesetText = "\n.$key {\n\t\t\n}"
val ruleset = CssElementFactory.getInstance(project).createRuleset(
rulesetText,
stylesheetFile.language
)

stylesheetFile.navigate(true)
stylesheetFile.add(ruleset)

val newEditor = FileEditorManager.getInstance(project).selectedEditor ?:return;
if(newEditor is TextEditor) {
val newEditor = FileEditorManager.getInstance(project).selectedEditor ?: return;

Check warning on line 38 in src/main/kotlin/com/peppa/css/annotator/SimpleCssSelectorFix.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant semicolon

Redundant semicolon
if (newEditor is TextEditor) {
newEditor.editor.caretModel.moveToLogicalPosition(
LogicalPosition(newEditor.editor.document.lineCount-2, 0)
LogicalPosition(newEditor.editor.document.lineCount - 2, 0)
)
newEditor.editor.scrollingModel.scrollTo(newEditor.editor.caretModel.logicalPosition,ScrollType.MAKE_VISIBLE)
DeclarativeInlayHintsPassFactory.scheduleRecompute(editor,project)
DeclarativeInlayHintsPassFactory.scheduleRecompute(newEditor.editor,project)
newEditor.editor.scrollingModel.scrollTo(
newEditor.editor.caretModel.logicalPosition,
ScrollType.MAKE_VISIBLE
)
DeclarativeInlayHintsPassFactory.scheduleRecompute(editor, project)
DeclarativeInlayHintsPassFactory.scheduleRecompute(newEditor.editor, project)
}

}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.peppa.css.completion

import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElement

Check warning on line 4 in src/main/kotlin/com/peppa/css/completion/CssModulesClassNameCompletionContributor.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.lang.ecmascript6.psi.ES6ImportedBinding
import com.intellij.lang.javascript.JavascriptLanguage
Expand All @@ -9,90 +9,72 @@
import com.intellij.lang.javascript.psi.JSLiteralExpression
import com.intellij.lang.javascript.psi.JSReferenceExpression
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiElement
import com.intellij.psi.css.StylesheetFile
import com.intellij.util.ProcessingContext

const val SplitChar = "-"
const val DotChar = "."
private const val SPLIT_CHAR = "-"
private const val DOT_CHAR = "."

class CssModulesClassNameCompletionContributor : CompletionContributor() {

init {
val language = PlatformPatterns
.psiElement()
.withLanguage(JavascriptLanguage.INSTANCE)
val jsPattern = PlatformPatterns.psiElement().withLanguage(JavascriptLanguage.INSTANCE)

extend(
CompletionType.BASIC,
language
jsPattern
.withParent(JSLiteralExpression::class.java)
.withSuperParent(2, JSIndexedPropertyAccessExpression::class.java),
CssModulesClassNameCompletionContributorProvider()
IndexAccessCompletionProvider()
)
extend(
CompletionType.BASIC,
language
.withParent(JSReferenceExpression::class.java),
CssModulesClassNameCompletionContributorWithDotProvider()
jsPattern.withParent(JSReferenceExpression::class.java),
DotAccessCompletionProvider()
)
}

private class CssModulesClassNameCompletionContributorProvider : CompletionProvider<CompletionParameters>() {
private class IndexAccessCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(
parameters: CompletionParameters,
context: ProcessingContext,
resultSet: CompletionResultSet
) {
val position = parameters.position
val stylesheetFile = findReferenceStyleFile(position.parent as JSLiteralExpression) ?: return
val literal = parameters.position.parent as? JSLiteralExpression ?: return
val stylesheetFile = findReferenceStyleFile(literal) ?: return
resultSet.addAllElements(generateLookupElementList(stylesheetFile))
}
}

private class CssModulesClassNameCompletionContributorWithDotProvider : CompletionProvider<CompletionParameters>() {
private class DotAccessCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(
parameters: CompletionParameters,
context: ProcessingContext,
resultSet: CompletionResultSet
) {

val position = parameters.position
if (position.prevSibling is PsiElement
&& position.prevSibling.text == DotChar
&& position.prevSibling.prevSibling is JSReferenceExpression
) {
val style = position.prevSibling.prevSibling
style.reference?.resolve()?.let { stylesFileImportStatement ->
if (stylesFileImportStatement !is ES6ImportedBinding || stylesFileImportStatement.findReferencedElements()
.isEmpty()
) return
val first = stylesFileImportStatement.findReferencedElements().firstOrNull()
first?.let {
resultSet.addAllElements(generateLookupElementList(it as StylesheetFile, true).map { element ->
// if choose completion with - , auto make to IndexedAccess
LookupElementDecorator.withInsertHandler(element) { context, item ->
StylesInsertHandler(item.lookupString.contains(SplitChar)).handleInsert(context, item)
}
})
val prevSibling = position.prevSibling ?: return
if (prevSibling.text != DOT_CHAR) return

val styleRef = prevSibling.prevSibling as? JSReferenceExpression ?: return
val binding = styleRef.reference?.resolve() as? ES6ImportedBinding ?: return
val stylesheetFile = binding.findReferencedElements().firstOrNull() as? StylesheetFile ?: return

val elements = generateLookupElementList(stylesheetFile, true).map { element ->
LookupElementDecorator.withInsertHandler(element) { ctx, item ->
if (item.lookupString.contains(SPLIT_CHAR)) {
convertToBracketSyntax(ctx, item.lookupString)
}
}
}
resultSet.addAllElements(elements)
}
}

private class StylesInsertHandler(private val needsBracketSyntax: Boolean) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
if (needsBracketSyntax) {
val editor = context.editor
val document = editor.document
val startOffset = context.startOffset
val dotPosOffset = startOffset - 1
val tailOffset = context.tailOffset
val lookupString = item.lookupString
document.replaceString(dotPosOffset, tailOffset, "[$lookupString]")
editor.caretModel.moveToOffset(dotPosOffset + lookupString.length + 2)
}
private fun convertToBracketSyntax(context: InsertionContext, lookupString: String) {
val document = context.editor.document
val dotOffset = context.startOffset - 1
document.replaceString(dotOffset, context.tailOffset, "['$lookupString']")
context.editor.caretModel.moveToOffset(dotOffset + lookupString.length + 4)
}
}

Expand Down
Loading
Loading