Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17
java-version: 21

# Setup Gradle
- name: Setup Gradle
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17
java-version: 21

# Setup Gradle
- name: Setup Gradle
Expand Down
15 changes: 12 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

# Alpine.js Support

## [Unreleased]
## [0.8.1] - 2026-06-02

### Changed
- Improved plugin auto-detection performance: the project is now scanned once for all plugins (instead of once per plugin), candidate files are located via the IDE's text index rather than reading every file, and `node_modules` is excluded from detection

## [0.8.0] - 2026-06-02

### Changed
- Improved support for [alpine-wizard](https://github.com/glhd/alpine-wizard)
Expand All @@ -12,7 +17,9 @@
- Added support for [alpine-ajax](https://alpine-ajax.js.org/)
- Added basic support for [alpine-tooltip](https://github.com/ryangjchandler/alpine-tooltip)
- Added configuration for plugins (enable/disable) when not auto-detected
- Added support for newer IntelliJ platforms
- Added support for PhpStorm / IntelliJ Platform 2026.1.2+ (removed the upper compatibility bound)
- Added quick documentation for Alpine directives
- Added inspection flagging the deprecated `x-spread` directive
- Added better local PhpStorm testing
- Added better handling of non-HTML file types
- Added new plugin extension system
Expand Down Expand Up @@ -151,7 +158,9 @@ A bug was introduced that impacted Alpine when you did something like `x-data="@
### Fixed
- Removed Alpine icon which seemed to cause issues for some people

[Unreleased]: https://github.com/inxilpro/IntellijAlpine/compare/v0.6.6...HEAD
[Unreleased]: https://github.com/inxilpro/IntellijAlpine/compare/v0.8.1...HEAD
[0.8.1]: https://github.com/inxilpro/IntellijAlpine/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/inxilpro/IntellijAlpine/compare/v0.6.6...v0.8.0
[0.6.6]: https://github.com/inxilpro/IntellijAlpine/compare/v0.6.5...v0.6.6
[0.6.5]: https://github.com/inxilpro/IntellijAlpine/compare/v0.6.4...v0.6.5
[0.6.4]: https://github.com/inxilpro/IntellijAlpine/compare/v0.6.3...v0.6.4
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
### Testing & Verification
- `./gradlew test` - Run unit tests
- `./gradlew check` - Run all checks (tests + verification)
- `./gradlew verifyPlugin` - Validate plugin structure and descriptors
- `./gradlew runPluginVerifier` - Check binary compatibility with target IDEs
- `./gradlew runInspections` - Run Qodana code inspections
- `./gradlew verifyPluginStructure` - Validate plugin structure and descriptors
- `./gradlew verifyPlugin` - Check binary compatibility with target IDEs (replaces 1.x `runPluginVerifier`)
- `./gradlew qodanaScan` - Run Qodana code inspections (requires Docker)
- `./gradlew koverReport` - Generate code coverage reports

## Architecture
Expand Down
11 changes: 8 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ dependencies {
providers.gradleProperty("platformBundledPlugins")
.map { it.split(',').map(String::trim).filter(String::isNotEmpty) })

// The JSON plugin was extracted into a separate module (2024.3+); package.json parsing needs it explicitly.
bundledModule("com.intellij.modules.json")

// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace.
plugins(
providers.gradleProperty("platformPlugins")
Expand Down Expand Up @@ -110,7 +113,8 @@ intellijPlatform {

ideaVersion {
sinceBuild = providers.gradleProperty("pluginSinceBuild")
untilBuild = providers.gradleProperty("pluginUntilBuild")
// No upper bound — keep the plugin compatible with future platform releases.
untilBuild = provider { null }
}
}

Expand All @@ -131,8 +135,9 @@ intellijPlatform {

pluginVerification {
ides {
// Only verify against the current version to speed up CI
ide("IU-2025.1")
// Verify against JetBrains' curated set of IDEs across the supported range,
// auto-tracking new platform releases since there's no upper compatibility bound.
recommended()
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ pluginGroup = com.github.inxilpro.intellijalpine
pluginName = Alpine.js Support
pluginRepositoryUrl = https://github.com/inxilpro/IntellijAlpine
# SemVer format -> https://semver.org
pluginVersion = 0.7.0
pluginVersion = 0.8.1

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# No upper bound: the plugin stays compatible with current and future platforms (verified via the plugin verifier).
pluginSinceBuild = 251
pluginUntilBuild = 253.*

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IU
platformVersion = 2025.1
platformVersion = 2026.1

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
Expand All @@ -21,7 +21,7 @@ platformPlugins =
platformBundledPlugins = JavaScript,HtmlTools

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.13
gradleVersion = 9.0.0

# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency = false
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ opentest4j = "1.3.0"

# plugins
changelog = "2.2.1"
intelliJPlatform = "2.6.0"
kotlin = "2.1.21"
intelliJPlatform = "2.16.0"
kotlin = "2.3.20"
kover = "0.9.1"
qodana = "2025.1.1"

Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class AlpineAttributeDescriptor(
return info.typeText
}

override fun init(psiElement: PsiElement) {}
@Suppress("WRONG_NULLABILITY_FOR_JAVA_OVERRIDE")
override fun init(psiElement: PsiElement?) {}

override fun isRequired(): Boolean = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.github.inxilpro.intellijalpine.core.detection.PluginDetector
import com.github.inxilpro.intellijalpine.settings.AlpineProjectSettingsState
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
Expand All @@ -14,6 +16,8 @@ import com.intellij.util.messages.MessageBusConnection
import org.apache.commons.lang3.tuple.MutablePair
import java.util.concurrent.ConcurrentHashMap

private val LOG = logger<AlpinePluginRegistry>()

@Service(Service.Level.APP)
class AlpinePluginRegistry {
private val listeners = ConcurrentHashMap<String, MessageBusConnection>()
Expand Down Expand Up @@ -72,24 +76,30 @@ class AlpinePluginRegistry {
}

fun checkAndAutoEnablePlugins(project: Project) {
getRegisteredPlugins().forEach { plugin ->
if (!isPluginEnabled(project, plugin.getPluginName()) && detector.detect(project, plugin)) {
enablePlugin(project, plugin.getPluginName())
}
}
if (DumbService.isDumb(project)) return

detectAndEnable(project, "for project '${project.name}'")
setupPackageJsonListener(project)
}

private fun detectAndEnable(project: Project, reason: String) {
val candidates = getRegisteredPlugins().filter { !isPluginEnabled(project, it.getPluginName()) }
if (candidates.isEmpty()) return

detector.detect(project, candidates).forEach { plugin ->
LOG.info("Auto-enabling Alpine plugin '${plugin.getPluginName()}' $reason")
enablePlugin(project, plugin.getPluginName())
}
}

private fun setupPackageJsonListener(project: Project) {
val projectPath = project.basePath ?: project.name

// Don't set up listener if already exists
if (listeners.containsKey(projectPath)) {
return
}

val connection = project.messageBus.connect()
val connection = project.messageBus.connect(project)
listeners[projectPath] = connection

connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
Expand All @@ -101,11 +111,9 @@ class AlpinePluginRegistry {

if (hasPackageJsonChanges) {
ApplicationManager.getApplication().executeOnPooledThread {
ApplicationManager.getApplication().runReadAction {
getRegisteredPlugins().forEach { plugin ->
if (!isPluginEnabled(project, plugin.getPluginName()) && detector.detect(project, plugin)) {
enablePlugin(project, plugin.getPluginName())
}
DumbService.getInstance(project).runWhenSmart {
ApplicationManager.getApplication().runReadAction {
detectAndEnable(project, "after package.json change")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.github.inxilpro.intellijalpine.core.detection

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.DelegatingGlobalSearchScope
import com.intellij.psi.search.GlobalSearchScope

internal object DetectionScopes {
fun projectScopeExcludingNodeModules(project: Project): GlobalSearchScope {
return object : DelegatingGlobalSearchScope(GlobalSearchScope.projectScope(project)) {
override fun contains(file: VirtualFile): Boolean {
return super.contains(file) && !isInNodeModules(file)
}
}
}

private fun isInNodeModules(file: VirtualFile): Boolean {
var current: VirtualFile? = file.parent
while (current != null) {
if (current.name == "node_modules") return true
current = current.parent
}
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import com.github.inxilpro.intellijalpine.core.AlpinePlugin
import com.intellij.openapi.project.Project

interface DetectionStrategy {
fun detect(project: Project, plugin: AlpinePlugin): Boolean
}
fun detect(project: Project, plugins: List<AlpinePlugin>): Set<AlpinePlugin>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,44 @@ package com.github.inxilpro.intellijalpine.core.detection
import com.github.inxilpro.intellijalpine.core.AlpinePlugin
import com.intellij.json.psi.JsonFile
import com.intellij.json.psi.JsonObject
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope

class PackageJsonDetector : DetectionStrategy {
override fun detect(project: Project, plugin: AlpinePlugin): Boolean {
override fun detect(project: Project, plugins: List<AlpinePlugin>): Set<AlpinePlugin> {
if (plugins.isEmpty() || DumbService.isDumb(project)) return emptySet()

val packageJsonFiles = FilenameIndex.getVirtualFilesByName(
"package.json",
GlobalSearchScope.projectScope(project)
DetectionScopes.projectScopeExcludingNodeModules(project)
)
if (packageJsonFiles.isEmpty()) return emptySet()

val psiManager = PsiManager.getInstance(project)
val detected = mutableSetOf<AlpinePlugin>()

return packageJsonFiles.any { virtualFile ->
val psiFile = PsiManager.getInstance(project).findFile(virtualFile)
if (psiFile is JsonFile) {
val rootObject = psiFile.topLevelValue as? JsonObject
val dependencies = rootObject?.findProperty("dependencies")?.value as? JsonObject
val devDependencies = rootObject?.findProperty("devDependencies")?.value as? JsonObject
for (virtualFile in packageJsonFiles) {
val psiFile = psiManager.findFile(virtualFile) as? JsonFile ?: continue
val rootObject = psiFile.topLevelValue as? JsonObject ?: continue
val dependencies = rootObject.findProperty("dependencies")?.value as? JsonObject
val devDependencies = rootObject.findProperty("devDependencies")?.value as? JsonObject

hasPluginDependency(plugin, dependencies) || hasPluginDependency(plugin, devDependencies)
} else {
false
for (plugin in plugins) {
if (plugin in detected) continue
if (hasPluginDependency(plugin, dependencies) || hasPluginDependency(plugin, devDependencies)) {
detected += plugin
}
}
}

return detected
}

private fun hasPluginDependency(plugin: AlpinePlugin, dependencies: JsonObject?): Boolean {
return plugin.getPackageNamesForDetection().any { packageName ->
dependencies?.findProperty(packageName) != null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,23 @@ class PluginDetector : DetectionStrategy {
ScriptReferenceDetector()
)

override fun detect(project: Project, plugin: AlpinePlugin): Boolean {
return strategies.any { it.detect(project, plugin) } || plugin.performDetection(project)
override fun detect(project: Project, plugins: List<AlpinePlugin>): Set<AlpinePlugin> {
if (plugins.isEmpty()) return emptySet()

val detected = mutableSetOf<AlpinePlugin>()

for (strategy in strategies) {
val remaining = plugins.filter { it !in detected }
if (remaining.isEmpty()) break
detected += strategy.detect(project, remaining)
}

for (plugin in plugins) {
if (plugin !in detected && plugin.performDetection(project)) {
detected += plugin
}
}

return detected
}
}
}
Loading