├── src
└── main
│ ├── resources
│ ├── messages
│ │ └── AlpineBundle.properties
│ ├── alpineicon.svg
│ └── META-INF
│ │ ├── pluginIcon.svg
│ │ └── plugin.xml
│ └── kotlin
│ └── com
│ └── github
│ └── inxilpro
│ └── intellijalpine
│ ├── Alpine.kt
│ ├── core
│ ├── detection
│ │ ├── DetectionStrategy.kt
│ │ ├── PluginDetector.kt
│ │ ├── PackageJsonDetector.kt
│ │ └── ScriptReferenceDetector.kt
│ ├── CompletionProviderRegistration.kt
│ ├── AlpinePlugin.kt
│ ├── AlpineLineMarkerProvider.kt
│ └── AlpinePluginRegistry.kt
│ ├── settings
│ ├── AlpineProjectListener.kt
│ ├── AlpineProjectActivity.kt
│ ├── AlpineSettingsState.kt
│ ├── AlpineProjectSettingsState.kt
│ ├── AlpineSettingsComponent.kt
│ └── AlpineSettingsConfigurable.kt
│ ├── attributes
│ ├── AttributesProvider.kt
│ ├── AlpineAttributeDescriptor.kt
│ ├── AttributeInfo.kt
│ └── AttributeUtil.kt
│ ├── completion
│ ├── AutoPopupHandler.kt
│ ├── AlpinePluginCompletionProviderWrapper.kt
│ ├── AlpineCompletionContributor.kt
│ ├── AlpineAttributeCompletionProvider.kt
│ └── AutoCompleteSuggestions.kt
│ ├── support
│ ├── XmlExtension.kt
│ └── LanguageUtil.kt
│ ├── plugins
│ ├── AlpineMergeValueCompletionProvider.kt
│ ├── TooltipPlugin.kt
│ ├── AlpineAjaxPlugin.kt
│ ├── AlpineTargetReferenceContributor.kt
│ └── AlpineWizardPlugin.kt
│ └── injection
│ └── AlpineJavaScriptAttributeValueInjector.kt
├── .gitignore
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── settings.gradle.kts
├── qodana.yml
├── .github
├── dependabot.yml
└── workflows
│ ├── run-ui-tests.yml
│ ├── release.yml
│ └── build.yml
├── .run
├── Run IDE for UI Tests.run.xml
├── Run IDE with Plugin.run.xml
├── Run Plugin Tests.run.xml
├── Run Qodana.run.xml
└── Run Plugin Verification.run.xml
├── LICENSE
├── gradle.properties
├── CLAUDE.md
├── gradlew.bat
├── README.md
├── CHANGELOG.md
└── gradlew
/src/main/resources/messages/AlpineBundle.properties:
--------------------------------------------------------------------------------
1 | name=Alpine.js
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | .intellijPlatform
4 | .kotlin
5 | .qodana
6 | build
7 | .DS_Store
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inxilpro/IntellijAlpine/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
3 | }
4 |
5 | rootProject.name = "IntellijAlpine"
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/Alpine.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine
2 |
3 | import com.intellij.openapi.util.IconLoader
4 |
5 | object Alpine {
6 | @JvmField
7 | val ICON = IconLoader.getIcon("/alpineicon.svg", javaClass)
8 | }
9 |
--------------------------------------------------------------------------------
/qodana.yml:
--------------------------------------------------------------------------------
1 | # Qodana configuration:
2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html
3 |
4 | version: "1.0"
5 | linter: jetbrains/qodana-jvm-community:2025.1
6 | projectJDK: "21"
7 | profile:
8 | name: qodana.recommended
9 | exclude:
10 | - name: All
11 | paths:
12 | - .qodana
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/DetectionStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core.detection
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
4 | import com.intellij.openapi.project.Project
5 |
6 | interface DetectionStrategy {
7 | fun detect(project: Project, plugin: AlpinePlugin): Boolean
8 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.settings
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
4 | import com.intellij.openapi.project.Project
5 | import com.intellij.openapi.project.ProjectManagerListener
6 |
7 | class AlpineProjectListener : ProjectManagerListener {
8 | override fun projectClosed(project: Project) {
9 | AlpinePluginRegistry.instance.cleanup(project)
10 | }
11 | }
--------------------------------------------------------------------------------
/src/main/resources/alpineicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Dependabot configuration:
2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | # Maintain dependencies for Gradle dependencies
7 | - package-ecosystem: "gradle"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 | # Maintain dependencies for GitHub Actions
12 | - package-ecosystem: "github-actions"
13 | directory: "/"
14 | schedule:
15 | interval: "daily"
16 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/pluginIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/CompletionProviderRegistration.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core
2 |
3 | import com.github.inxilpro.intellijalpine.completion.AlpinePluginCompletionProvider
4 | import com.intellij.codeInsight.completion.CompletionType
5 | import com.intellij.patterns.ElementPattern
6 | import com.intellij.psi.PsiElement
7 |
8 | data class CompletionProviderRegistration(
9 | val pattern: ElementPattern,
10 | val provider: AlpinePluginCompletionProvider,
11 | val type: CompletionType = CompletionType.BASIC,
12 | )
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.settings
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
4 | import com.intellij.openapi.application.readAction
5 | import com.intellij.openapi.project.Project
6 | import com.intellij.openapi.startup.ProjectActivity
7 |
8 | class AlpineProjectActivity : ProjectActivity {
9 | override suspend fun execute(project: Project) {
10 | readAction {
11 | AlpinePluginRegistry.instance.checkAndAutoEnablePlugins(project)
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core.detection
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
4 | import com.intellij.openapi.project.Project
5 |
6 | class PluginDetector : DetectionStrategy {
7 | private val strategies: List = listOf(
8 | PackageJsonDetector(),
9 | ScriptReferenceDetector()
10 | )
11 |
12 | override fun detect(project: Project, plugin: AlpinePlugin): Boolean {
13 | return strategies.any { it.detect(project, plugin) } || plugin.performDetection(project)
14 | }
15 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | # libraries
3 | junit = "4.13.2"
4 | opentest4j = "1.3.0"
5 |
6 | # plugins
7 | changelog = "2.2.1"
8 | intelliJPlatform = "2.6.0"
9 | kotlin = "2.1.21"
10 | kover = "0.9.1"
11 | qodana = "2025.1.1"
12 |
13 | [libraries]
14 | junit = { group = "junit", name = "junit", version.ref = "junit" }
15 | opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" }
16 |
17 | [plugins]
18 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
19 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" }
20 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
21 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
22 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" }
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributesProvider.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.attributes
2 |
3 | import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl
4 | import com.intellij.psi.xml.XmlTag
5 | import com.intellij.xml.XmlAttributeDescriptor
6 | import com.intellij.xml.XmlAttributeDescriptorsProvider
7 |
8 | class AttributesProvider : XmlAttributeDescriptorsProvider {
9 | override fun getAttributeDescriptors(xmlTag: XmlTag): Array {
10 | return emptyArray()
11 | }
12 |
13 | @Suppress("ReturnCount")
14 | override fun getAttributeDescriptor(name: String, xmlTag: XmlTag): XmlAttributeDescriptor? {
15 | if (xmlTag.descriptor !is HtmlElementDescriptorImpl) return null
16 | val info = AttributeInfo(name)
17 |
18 | if (info.isAlpine()) {
19 | return AlpineAttributeDescriptor(name, xmlTag)
20 | }
21 |
22 | return null
23 | }
24 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsState.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.settings
2 |
3 | import com.intellij.openapi.application.ApplicationManager
4 | import com.intellij.openapi.components.PersistentStateComponent
5 | import com.intellij.openapi.components.State
6 | import com.intellij.openapi.components.Storage
7 | import com.intellij.util.xmlb.XmlSerializerUtil
8 |
9 | @State(name = "com.github.inxilpro.intellijalpine.AppSettingsState", storages = [Storage("IntellijAlpine.xml")])
10 | class AlpineSettingsState : PersistentStateComponent {
11 | var showGutterIcons = true
12 |
13 | override fun getState(): AlpineSettingsState? {
14 | return this
15 | }
16 |
17 | override fun loadState(state: AlpineSettingsState) {
18 | XmlSerializerUtil.copyBean(state, this)
19 | }
20 |
21 | companion object {
22 | val instance: AlpineSettingsState
23 | get() = ApplicationManager.getApplication().getService(AlpineSettingsState::class.java)
24 | }
25 | }
--------------------------------------------------------------------------------
/.run/Run IDE for UI Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
15 |
16 |
17 | true
18 | true
19 | false
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/Run IDE with Plugin.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.run/Run Plugin Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.run/Run Qodana.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | true
22 | true
23 | false
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2023 Chris Morrell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoPopupHandler.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.completion
2 |
3 | import com.intellij.codeInsight.AutoPopupController
4 | import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
5 | import com.intellij.codeInsight.lookup.LookupManager
6 | import com.intellij.openapi.editor.Editor
7 | import com.intellij.openapi.project.Project
8 | import com.intellij.psi.PsiFile
9 | import com.intellij.psi.html.HtmlTag
10 |
11 | class AutoPopupHandler : TypedHandlerDelegate() {
12 | @Suppress("ReturnCount")
13 | override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result {
14 | if (LookupManager.getActiveLookup(editor) != null) {
15 | return Result.CONTINUE
16 | }
17 |
18 | val element = file.findElementAt(editor.caretModel.offset)
19 | if (element?.parent !is HtmlTag) {
20 | return Result.CONTINUE
21 | }
22 |
23 | if (charTyped == '@' || charTyped == ':' || charTyped == '.') {
24 | AutoPopupController.getInstance(project).scheduleAutoPopup(editor)
25 | return Result.STOP
26 | }
27 |
28 | return Result.CONTINUE
29 | }
30 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.completion
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
4 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
5 | import com.intellij.codeInsight.completion.CompletionParameters
6 | import com.intellij.codeInsight.completion.CompletionProvider
7 | import com.intellij.codeInsight.completion.CompletionResultSet
8 | import com.intellij.util.ProcessingContext
9 |
10 | abstract class AlpinePluginCompletionProvider(
11 | private val plugin: AlpinePlugin
12 | ) : CompletionProvider() {
13 |
14 | final override fun addCompletions(
15 | parameters: CompletionParameters,
16 | context: ProcessingContext,
17 | result: CompletionResultSet
18 | ) {
19 | if (AlpinePluginRegistry.instance.isPluginEnabled(parameters.position.project, plugin)) {
20 | addPluginCompletions(parameters, context, result)
21 | }
22 | }
23 |
24 | protected abstract fun addPluginCompletions(
25 | parameters: CompletionParameters,
26 | context: ProcessingContext,
27 | result: CompletionResultSet
28 | )
29 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.completion
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
4 | import com.intellij.codeInsight.completion.CompletionContributor
5 | import com.intellij.codeInsight.completion.CompletionType
6 | import com.intellij.patterns.PlatformPatterns
7 | import com.intellij.patterns.XmlPatterns
8 | import com.intellij.psi.xml.XmlTokenType
9 |
10 | class AlpineCompletionContributor : CompletionContributor() {
11 | init {
12 | // Attribute name completion
13 | extend(
14 | CompletionType.BASIC,
15 | PlatformPatterns.psiElement(XmlTokenType.XML_NAME).withParent(XmlPatterns.xmlAttribute()),
16 | AlpineAttributeCompletionProvider()
17 | )
18 |
19 | // Plugin completions
20 | AlpinePluginRegistry.instance.getRegisteredPlugins().forEach { plugin ->
21 | plugin.getCompletionProviders().forEach { registration ->
22 | extend(
23 | registration.type,
24 | registration.pattern,
25 | registration.provider
26 | )
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/.run/Run Plugin Verification.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core
2 |
3 | import com.github.inxilpro.intellijalpine.attributes.AttributeInfo
4 | import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions
5 | import com.intellij.openapi.extensions.ExtensionPointName
6 | import com.intellij.openapi.project.Project
7 | import org.apache.commons.lang3.tuple.MutablePair
8 |
9 | interface AlpinePlugin {
10 | companion object {
11 | val EP_NAME =
12 | ExtensionPointName.Companion.create("com.github.inxilpro.intellijalpine.alpinePlugin")
13 | }
14 |
15 | fun getPluginName(): String
16 |
17 | fun getPackageDisplayName(): String
18 |
19 | fun getPackageNamesForDetection(): List
20 |
21 | fun getTypeText(info: AttributeInfo): String? = null
22 |
23 | fun injectJsContext(context: MutablePair): MutablePair = context
24 |
25 | fun directiveSupportJavaScript(directive: String): Boolean = true
26 |
27 | fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {}
28 |
29 | fun getCompletionProviders(): List = emptyList()
30 |
31 | fun getDirectives(): List = emptyList()
32 |
33 | fun getPrefixes(): List = emptyList()
34 |
35 | fun performDetection(project: Project): Boolean = false
36 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.settings
2 |
3 | import com.intellij.openapi.components.PersistentStateComponent
4 | import com.intellij.openapi.components.State
5 | import com.intellij.openapi.components.Storage
6 | import com.intellij.openapi.project.Project
7 | import com.intellij.util.xmlb.XmlSerializerUtil
8 |
9 | @State(
10 | name = "com.github.inxilpro.intellijalpine.AlpineProjectSettingsState",
11 | storages = [Storage("alpinejs-support.xml")]
12 | )
13 | class AlpineProjectSettingsState : PersistentStateComponent {
14 | var enabledPlugins = mutableMapOf()
15 |
16 | fun isPluginEnabled(pluginName: String): Boolean {
17 | return enabledPlugins[pluginName] ?: false
18 | }
19 |
20 | fun setPluginEnabled(pluginName: String, enabled: Boolean) {
21 | enabledPlugins[pluginName] = enabled
22 | }
23 |
24 | override fun getState(): AlpineProjectSettingsState? {
25 | return this
26 | }
27 |
28 | override fun loadState(state: AlpineProjectSettingsState) {
29 | XmlSerializerUtil.copyBean(state, this)
30 | }
31 |
32 | companion object {
33 | fun getInstance(project: Project): AlpineProjectSettingsState {
34 | return project.getService(AlpineProjectSettingsState::class.java)
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.support
2 |
3 | import com.github.inxilpro.intellijalpine.attributes.AttributeUtil
4 | import com.intellij.openapi.util.TextRange
5 | import com.intellij.psi.PsiFile
6 | import com.intellij.psi.html.HtmlTag
7 | import com.intellij.psi.impl.source.xml.SchemaPrefix
8 | import com.intellij.psi.xml.XmlTag
9 | import com.intellij.xml.HtmlXmlExtension
10 |
11 | class XmlExtension : HtmlXmlExtension() {
12 | override fun isAvailable(file: PsiFile?): Boolean {
13 | if (file == null) return false
14 | return LanguageUtil.supportsAlpineJs(file)
15 | }
16 |
17 | override fun getPrefixDeclaration(context: XmlTag, namespacePrefix: String?): SchemaPrefix? {
18 | if (null != namespacePrefix && context is HtmlTag && hasAlpinePrefix(namespacePrefix)) {
19 | findAttributeSchema(context, namespacePrefix)
20 | ?.let { return it }
21 | }
22 |
23 | return super.getPrefixDeclaration(context, namespacePrefix)
24 | }
25 |
26 | private fun hasAlpinePrefix(namespacePrefix: String): Boolean {
27 | return AttributeUtil.isXmlPrefix(namespacePrefix)
28 | }
29 |
30 | private fun findAttributeSchema(context: XmlTag, namespacePrefix: String): SchemaPrefix? {
31 | return context.attributes
32 | .find { it.name.startsWith(namespacePrefix) }
33 | ?.let { SchemaPrefix(it, TextRange.create(0, namespacePrefix.length), "Alpine.js") }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.attributes
2 |
3 | import com.github.inxilpro.intellijalpine.Alpine
4 | import com.intellij.psi.PsiElement
5 | import com.intellij.psi.meta.PsiPresentableMetaData
6 | import com.intellij.psi.xml.XmlTag
7 | import com.intellij.util.ArrayUtil
8 | import com.intellij.xml.impl.BasicXmlAttributeDescriptor
9 |
10 | class AlpineAttributeDescriptor(
11 | private val name: String,
12 | private val xmlTag: XmlTag
13 | ) :
14 | BasicXmlAttributeDescriptor(),
15 | PsiPresentableMetaData {
16 |
17 | private val info: AttributeInfo = AttributeInfo(name)
18 |
19 | override fun getIcon() = Alpine.ICON
20 |
21 | override fun getTypeName(): String {
22 | return info.typeText
23 | }
24 |
25 | override fun init(psiElement: PsiElement) {}
26 |
27 | override fun isRequired(): Boolean = false
28 |
29 | override fun hasIdType(): Boolean {
30 | return name == "id"
31 | }
32 |
33 | override fun hasIdRefType(): Boolean = false
34 |
35 | override fun isEnumerated(): Boolean {
36 | return !info.hasValue()
37 | }
38 |
39 | override fun getDeclaration(): PsiElement? = xmlTag
40 |
41 | override fun getName(): String = name
42 |
43 | override fun getDependencies(): Array = ArrayUtil.EMPTY_OBJECT_ARRAY
44 |
45 | override fun isFixed(): Boolean = false
46 |
47 | override fun getDefaultValue(): String? = null
48 |
49 | override fun getEnumeratedValues(): Array? = ArrayUtil.EMPTY_STRING_ARRAY
50 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core
2 |
3 | import com.github.inxilpro.intellijalpine.Alpine
4 | import com.github.inxilpro.intellijalpine.attributes.AlpineAttributeDescriptor
5 | import com.github.inxilpro.intellijalpine.settings.AlpineSettingsState
6 | import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo
7 | import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider
8 | import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder
9 | import com.intellij.psi.PsiElement
10 | import com.intellij.psi.impl.source.xml.XmlTokenImpl
11 | import com.intellij.psi.util.PsiTreeUtil
12 | import com.intellij.psi.xml.XmlAttribute
13 | import javax.swing.Icon
14 |
15 | class AlpineLineMarkerProvider : RelatedItemLineMarkerProvider() {
16 | override fun getIcon(): Icon {
17 | return Alpine.ICON
18 | }
19 |
20 | override fun collectNavigationMarkers(
21 | element: PsiElement,
22 | result: MutableCollection?>
23 | ) {
24 | if (!AlpineSettingsState.instance.showGutterIcons) return
25 |
26 | if (element is XmlAttribute && element.descriptor is AlpineAttributeDescriptor) {
27 |
28 | val token = PsiTreeUtil.getChildOfType(element, XmlTokenImpl::class.java) ?: return
29 |
30 | val builder = NavigationGutterIconBuilder.create(Alpine.ICON)
31 | .setTarget(token)
32 | .setTooltipText("Alpine.js directive")
33 |
34 | result.add(builder.createLineMarkerInfo(token))
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PackageJsonDetector.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core.detection
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
4 | import com.intellij.json.psi.JsonFile
5 | import com.intellij.json.psi.JsonObject
6 | import com.intellij.openapi.project.Project
7 | import com.intellij.psi.PsiManager
8 | import com.intellij.psi.search.FilenameIndex
9 | import com.intellij.psi.search.GlobalSearchScope
10 |
11 | class PackageJsonDetector : DetectionStrategy {
12 | override fun detect(project: Project, plugin: AlpinePlugin): Boolean {
13 | val packageJsonFiles = FilenameIndex.getVirtualFilesByName(
14 | "package.json",
15 | GlobalSearchScope.projectScope(project)
16 | )
17 |
18 | return packageJsonFiles.any { virtualFile ->
19 | val psiFile = PsiManager.getInstance(project).findFile(virtualFile)
20 | if (psiFile is JsonFile) {
21 | val rootObject = psiFile.topLevelValue as? JsonObject
22 | val dependencies = rootObject?.findProperty("dependencies")?.value as? JsonObject
23 | val devDependencies = rootObject?.findProperty("devDependencies")?.value as? JsonObject
24 |
25 | hasPluginDependency(plugin, dependencies) || hasPluginDependency(plugin, devDependencies)
26 | } else {
27 | false
28 | }
29 | }
30 | }
31 |
32 | private fun hasPluginDependency(plugin: AlpinePlugin, dependencies: JsonObject?): Boolean {
33 | return plugin.getPackageNamesForDetection().any { packageName ->
34 | dependencies?.findProperty(packageName) != null
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
2 |
3 | pluginGroup = com.github.inxilpro.intellijalpine
4 | pluginName = Alpine.js Support
5 | pluginRepositoryUrl = https://github.com/inxilpro/IntellijAlpine
6 | # SemVer format -> https://semver.org
7 | pluginVersion = 0.7.0
8 |
9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
10 | pluginSinceBuild = 251
11 | pluginUntilBuild = 253.*
12 |
13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
14 | platformType = IU
15 | platformVersion = 2025.1
16 |
17 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
18 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
19 | platformPlugins =
20 | # Example: platformBundledPlugins = com.intellij.java
21 | platformBundledPlugins = JavaScript,HtmlTools
22 |
23 | # Gradle Releases -> https://github.com/gradle/gradle/releases
24 | gradleVersion = 8.13
25 |
26 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
27 | kotlin.stdlib.default.dependency = false
28 |
29 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
30 | org.gradle.configuration-cache = true
31 |
32 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
33 | org.gradle.caching = true
34 |
35 | # Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
36 | systemProp.org.gradle.unsafe.kotlin.assignment = true
37 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.plugins
2 |
3 | import com.github.inxilpro.intellijalpine.Alpine
4 | import com.github.inxilpro.intellijalpine.completion.AlpinePluginCompletionProvider
5 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
6 | import com.intellij.codeInsight.completion.CompletionParameters
7 | import com.intellij.codeInsight.completion.CompletionResultSet
8 | import com.intellij.codeInsight.lookup.LookupElementBuilder
9 | import com.intellij.psi.util.PsiTreeUtil
10 | import com.intellij.psi.xml.XmlAttribute
11 | import com.intellij.util.ProcessingContext
12 |
13 | class AlpineMergeValueCompletionProvider(
14 | plugin: AlpinePlugin
15 | ) : AlpinePluginCompletionProvider(plugin) {
16 |
17 | private val mergeStrategies = arrayOf(
18 | "before" to "Insert content before target",
19 | "replace" to "Replace target element (default)",
20 | "update" to "Update target's innerHTML",
21 | "prepend" to "Prepend content to target",
22 | "append" to "Append content to target",
23 | "after" to "Insert content after target",
24 | "morph" to "Morph content preserving state"
25 | )
26 |
27 | override fun addPluginCompletions(
28 | parameters: CompletionParameters,
29 | context: ProcessingContext,
30 | result: CompletionResultSet
31 | ) {
32 | val element = parameters.position
33 | val attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute::class.java) ?: return
34 |
35 | if (attribute.name != "x-merge") {
36 | return
37 | }
38 |
39 | for ((strategy, description) in mergeStrategies) {
40 | val lookupElement = LookupElementBuilder
41 | .create(strategy)
42 | .withTypeText(description)
43 | .withIcon(Alpine.ICON)
44 |
45 | result.addElement(lookupElement)
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/.github/workflows/run-ui-tests.yml:
--------------------------------------------------------------------------------
1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps:
2 | # - prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with UI
3 | # - wait for IDE to start
4 | # - run UI tests with separate Gradle task
5 | #
6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform
7 | #
8 | # Workflow is triggered manually.
9 |
10 | name: Run UI Tests
11 | on:
12 | workflow_dispatch
13 |
14 | jobs:
15 |
16 | testUI:
17 | runs-on: ${{ matrix.os }}
18 | timeout-minutes: 45
19 | strategy:
20 | fail-fast: false
21 | matrix:
22 | include:
23 | - os: ubuntu-latest
24 | runIde: |
25 | export DISPLAY=:99.0
26 | Xvfb -ac :99 -screen 0 1920x1080x16 &
27 | gradle runIdeForUiTests &
28 | - os: windows-latest
29 | runIde: start gradlew.bat runIdeForUiTests
30 | - os: macos-latest
31 | runIde: ./gradlew runIdeForUiTests &
32 |
33 | steps:
34 |
35 | # Check out current repository
36 | - name: Fetch Sources
37 | uses: actions/checkout@v4
38 |
39 | # Setup Java environment for the next steps
40 | - name: Setup Java
41 | uses: actions/setup-java@v4
42 | with:
43 | distribution: zulu
44 | java-version: 17
45 |
46 | # Setup Gradle
47 | - name: Setup Gradle
48 | uses: gradle/gradle-build-action@v3
49 | with:
50 | gradle-home-cache-cleanup: true
51 |
52 | # Run IDEA prepared for UI testing
53 | - name: Run IDE
54 | run: ${{ matrix.runIde }}
55 |
56 | # Wait for IDEA to be started
57 | - name: Health Check
58 | uses: jtalk/url-health-check-action@v4
59 | with:
60 | url: http://127.0.0.1:8082
61 | max-attempts: 15
62 | retry-delay: 30s
63 |
64 | # Run tests
65 | - name: Tests
66 | run: ./gradlew test
67 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.plugins
2 |
3 | import com.github.inxilpro.intellijalpine.attributes.AttributeInfo
4 | import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions
5 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
6 | import org.apache.commons.lang3.tuple.MutablePair
7 |
8 | class TooltipPlugin : AlpinePlugin {
9 |
10 | override fun getPluginName(): String = "alpine-tooltip"
11 |
12 | override fun getPackageDisplayName(): String = "alpine-tooltip"
13 |
14 | override fun getPackageNamesForDetection(): List = listOf(
15 | "alpine-tooltip",
16 | "@ryangjchandler/alpine-tooltip"
17 | )
18 |
19 | override fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {
20 | val modifiers = arrayOf(
21 | "duration",
22 | "delay",
23 | "cursor",
24 | "on",
25 | "arrowless",
26 | "html",
27 | "interactive",
28 | "border",
29 | "debounce",
30 | "max-width",
31 | "theme",
32 | "placement",
33 | "animation",
34 | "no-flip",
35 | )
36 |
37 | suggestions.addModifiers("x-tooltip", modifiers)
38 | }
39 |
40 | override fun getTypeText(info: AttributeInfo): String? {
41 | return when (info.attribute) {
42 | "x-tooltip" -> "Tippy.js tooltip"
43 | else -> null
44 | }
45 | }
46 |
47 | override fun injectJsContext(context: MutablePair): MutablePair {
48 | val magics = """
49 | /**
50 | * @param {string} value
51 | * @param {Object} options
52 | * @return {Promise}
53 | */
54 | function ${'$'}tooltip(value, options = {}) {}
55 |
56 | """.trimIndent()
57 |
58 | return MutablePair(context.left + magics, context.right)
59 | }
60 |
61 | override fun getDirectives(): List = listOf(
62 | "x-tooltip",
63 | )
64 | }
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Code Guidelines
6 |
7 | - Always use modern idiomatic Kotlin code
8 | - When implementing singletons, prefer `Foo.instance` over `Foo.Companion.instance` or `Foo.getInstance()`
9 | - Only add docblocks and comments when they provide substantive value. Comments should always explain "why" not "what."
10 |
11 | ## Commands
12 |
13 | ### Building & Running
14 | - `./gradlew build` - Build the plugin
15 | - `./gradlew buildPlugin` - Assemble plugin ZIP for deployment
16 | - `./gradlew runIde` - Run IntelliJ IDEA with the plugin installed for testing
17 | - `./gradlew runIdeForUiTests` - Run IDE with robot-server for UI testing
18 |
19 | ### Testing & Verification
20 | - `./gradlew test` - Run unit tests
21 | - `./gradlew check` - Run all checks (tests + verification)
22 | - `./gradlew verifyPlugin` - Validate plugin structure and descriptors
23 | - `./gradlew runPluginVerifier` - Check binary compatibility with target IDEs
24 | - `./gradlew runInspections` - Run Qodana code inspections
25 | - `./gradlew koverReport` - Generate code coverage reports
26 |
27 | ## Architecture
28 |
29 | This is an IntelliJ IDEA plugin that adds Alpine.js support. The plugin provides:
30 |
31 | - Auto-completion for Alpine directives (x-data, x-show, x-model, etc.)
32 | - JavaScript language injection in Alpine attributes
33 | - Syntax highlighting within Alpine directives
34 | - Plugin support for third-party alpine plugins
35 |
36 | ### Plugin Configuration
37 |
38 | The plugin is configured via:
39 |
40 | - `plugin.xml` - Main plugin manifest defining extensions and dependencies
41 | - `gradle.properties` - Version and platform configuration
42 | - `build.gradle.kts` - Build configuration and dependencies
43 |
44 | The plugin requires:
45 |
46 | - IntelliJ IDEA 2025.1 or newer
47 | - JavaScript and HtmlTools plugins as dependencies
48 | - Java 21 runtime
49 |
50 | ### Release Process
51 |
52 | 1. Update version in `gradle.properties`
53 | 2. Update `CHANGELOG.md` Unreleased section with version number
54 | 3. Push changes to main branch
55 | 4. Create and publish a GitHub release - this triggers automatic publishing to JetBrains Marketplace
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.support
2 |
3 | import com.intellij.lang.Language
4 | import com.intellij.lang.html.HTMLLanguage
5 | import com.intellij.lang.xml.XMLLanguage
6 | import com.intellij.psi.PsiFile
7 | import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider
8 |
9 | object LanguageUtil {
10 |
11 | private val HTML_LIKE_EXTENSIONS = setOf(
12 | "html", "htm", "xhtml", "xml",
13 | "php", "twig", "smarty", "tpl", "phtml",
14 | "erb", "jsp", "jsf", "ftl", "vm",
15 | )
16 |
17 | private val TEMPLATE_LANGUAGE_IDS = setOf(
18 | "Blade", "PHP", "Twig", "Smarty", "FreeMarker",
19 | "Velocity", "JSP", "ERB",
20 | )
21 |
22 | fun supportsAlpineJs(file: PsiFile): Boolean {
23 | return hasHtmlBasedLanguage(file) || hasTemplateLanguage(file) || hasHtmlLikeExtension(file) || isTemplateLanguageFile(
24 | file
25 | )
26 | }
27 |
28 | fun hasPhpLanguage(file: PsiFile): Boolean {
29 | return file.viewProvider.languages.any { lang ->
30 | lang.id == "PHP" || lang.id == "Blade"
31 | }
32 | }
33 |
34 | private fun hasHtmlBasedLanguage(file: PsiFile): Boolean {
35 | return file.viewProvider.languages.any { lang ->
36 | isHtmlBasedLanguage(lang)
37 | }
38 | }
39 |
40 | private fun isHtmlBasedLanguage(language: Language): Boolean {
41 | if (language.isKindOf(HTMLLanguage.INSTANCE)) {
42 | return true
43 | }
44 |
45 | if (language.isKindOf(XMLLanguage.INSTANCE)) {
46 | return TEMPLATE_LANGUAGE_IDS.contains(language.id)
47 | }
48 |
49 | return false
50 | }
51 |
52 | private fun hasTemplateLanguage(file: PsiFile): Boolean {
53 | return file.viewProvider.languages.any { lang ->
54 | TEMPLATE_LANGUAGE_IDS.contains(lang.id)
55 | }
56 | }
57 |
58 | private fun hasHtmlLikeExtension(file: PsiFile): Boolean {
59 | val fileName = file.name.lowercase()
60 | return HTML_LIKE_EXTENSIONS.any { ext ->
61 | fileName.endsWith(".$ext")
62 | }
63 | }
64 |
65 | private fun isTemplateLanguageFile(file: PsiFile): Boolean {
66 | return file.viewProvider is TemplateLanguageFileViewProvider
67 | }
68 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.completion
2 |
3 | import com.github.inxilpro.intellijalpine.Alpine
4 | import com.github.inxilpro.intellijalpine.support.LanguageUtil
5 | import com.intellij.codeInsight.completion.CompletionParameters
6 | import com.intellij.codeInsight.completion.CompletionProvider
7 | import com.intellij.codeInsight.completion.CompletionResultSet
8 | import com.intellij.codeInsight.completion.CompletionUtilCore
9 | import com.intellij.codeInsight.completion.XmlAttributeInsertHandler
10 | import com.intellij.codeInsight.lookup.LookupElementBuilder
11 | import com.intellij.openapi.util.text.StringUtil
12 | import com.intellij.psi.html.HtmlTag
13 | import com.intellij.psi.xml.XmlAttribute
14 | import com.intellij.util.ProcessingContext
15 |
16 | class AlpineAttributeCompletionProvider(vararg items: String) : CompletionProvider() {
17 |
18 | @Suppress("ReturnCount")
19 | public override fun addCompletions(
20 | parameters: CompletionParameters,
21 | context: ProcessingContext,
22 | result: CompletionResultSet
23 | ) {
24 | val position = parameters.position
25 |
26 | if (!LanguageUtil.supportsAlpineJs(position.containingFile)) {
27 | return
28 | }
29 |
30 | val attribute = position.parent as? XmlAttribute ?: return
31 | val xmlTag = attribute.parent as? HtmlTag ?: return
32 |
33 | val partialAttribute = StringUtil.trimEnd(attribute.name, CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED)
34 |
35 | if (partialAttribute.isEmpty()) {
36 | return
37 | }
38 |
39 | val suggestions = AutoCompleteSuggestions(xmlTag, partialAttribute)
40 |
41 | suggestions.descriptors.forEach {
42 | var text = it.attribute
43 |
44 | // If you go back and add a modifier, it ignores the prefix, so we'll
45 | // just kinda code around that for now
46 | if (text.contains(':') && text.contains('.')) {
47 | text = text.substringAfter(':')
48 | }
49 |
50 | var elementBuilder = LookupElementBuilder
51 | .create(text)
52 | .withCaseSensitivity(false)
53 | .withIcon(Alpine.ICON)
54 | .withTypeText(it.typeText)
55 |
56 | if (it.hasValue() && !it.canBePrefix()) {
57 | elementBuilder = elementBuilder.withInsertHandler(XmlAttributeInsertHandler.INSTANCE)
58 | }
59 |
60 | result.addElement(elementBuilder)
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.settings
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
4 | import com.intellij.openapi.project.Project
5 | import com.intellij.ui.TitledSeparator
6 | import com.intellij.ui.components.JBCheckBox
7 | import com.intellij.ui.components.JBLabel
8 | import com.intellij.util.ui.FormBuilder
9 | import com.intellij.util.ui.UIUtil
10 | import javax.swing.JComponent
11 | import javax.swing.JPanel
12 |
13 | class AlpineSettingsComponent(project: Project?) {
14 | val panel: JPanel
15 |
16 | private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons")
17 | private val pluginCheckBoxes = mutableMapOf()
18 |
19 | val preferredFocusedComponent: JComponent
20 | get() = myShowGutterIconsStatus
21 |
22 | var showGutterIconsStatus: Boolean
23 | get() = myShowGutterIconsStatus.isSelected
24 | set(newStatus) {
25 | myShowGutterIconsStatus.isSelected = newStatus
26 | }
27 |
28 | fun getPluginStatus(pluginName: String): Boolean {
29 | return pluginCheckBoxes[pluginName]?.isSelected ?: false
30 | }
31 |
32 | fun setPluginStatus(pluginName: String, enabled: Boolean) {
33 | pluginCheckBoxes[pluginName]?.isSelected = enabled
34 | }
35 |
36 | init {
37 | val builder = FormBuilder.createFormBuilder()
38 | .addComponent(TitledSeparator("Plugin Settings"))
39 | .addComponent(myShowGutterIconsStatus, 1)
40 |
41 | // Only show project settings if we have a project context
42 | if (project != null) {
43 | builder.addVerticalGap(10) // Add spacing between sections
44 | .addComponent(TitledSeparator("Project Settings for “${project.name}”"))
45 |
46 | val projectLabel = JBLabel("These settings apply only to the current project")
47 | projectLabel.foreground = UIUtil.getContextHelpForeground()
48 | projectLabel.font = UIUtil.getLabelFont(UIUtil.FontSize.SMALL)
49 | builder.addComponent(projectLabel, 1)
50 | .addVerticalGap(5)
51 |
52 | // Dynamically add checkboxes for each registered plugin
53 | AlpinePluginRegistry.instance.getRegisteredPlugins().forEach { plugin ->
54 | val checkBox = JBCheckBox("Enable “${plugin.getPackageDisplayName()}” support for this project")
55 | pluginCheckBoxes[plugin.getPluginName()] = checkBox
56 | builder.addComponent(checkBox, 1)
57 | }
58 | }
59 |
60 | panel = builder.addComponentFillVertically(JPanel(), 0).panel
61 | }
62 | }
--------------------------------------------------------------------------------
/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 https://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 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.settings
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
4 | import com.intellij.openapi.options.Configurable
5 | import com.intellij.openapi.project.Project
6 | import javax.swing.JComponent
7 |
8 | class AlpineSettingsConfigurable(private val project: Project?) : Configurable {
9 | private var mySettingsComponent: AlpineSettingsComponent? = null
10 |
11 | @Suppress("DialogTitleCapitalization")
12 | override fun getDisplayName(): String {
13 | return "Alpine.js"
14 | }
15 |
16 | override fun getPreferredFocusedComponent(): JComponent? {
17 | return mySettingsComponent?.preferredFocusedComponent
18 | }
19 |
20 | override fun createComponent(): JComponent? {
21 | mySettingsComponent = AlpineSettingsComponent(project)
22 | return mySettingsComponent?.panel
23 | }
24 |
25 | override fun isModified(): Boolean {
26 | val appSettings = AlpineSettingsState.instance
27 | var isModified = mySettingsComponent?.showGutterIconsStatus != appSettings.showGutterIcons
28 |
29 | // Check project settings if we have a project
30 | if (project != null) {
31 | val registry = AlpinePluginRegistry.instance
32 | registry.getRegisteredPlugins().forEach { plugin ->
33 | val pluginName = plugin.getPluginName()
34 | val currentStatus = mySettingsComponent?.getPluginStatus(pluginName) ?: false
35 | val savedStatus = registry.isPluginEnabled(project, pluginName)
36 | if (currentStatus != savedStatus) {
37 | isModified = true
38 | }
39 | }
40 | }
41 |
42 | return isModified
43 | }
44 |
45 | override fun apply() {
46 | val appSettings = AlpineSettingsState.instance
47 | appSettings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false
48 |
49 | // Apply project settings if we have a project
50 | if (project != null) {
51 | val registry = AlpinePluginRegistry.instance
52 | registry.getRegisteredPlugins().forEach { plugin ->
53 | val pluginName = plugin.getPluginName()
54 | val enabled = mySettingsComponent?.getPluginStatus(pluginName) ?: false
55 | if (enabled) {
56 | registry.enablePlugin(project, pluginName)
57 | } else {
58 | registry.disablePlugin(project, pluginName)
59 | }
60 | }
61 | }
62 | }
63 |
64 | override fun reset() {
65 | val appSettings = AlpineSettingsState.instance
66 | mySettingsComponent?.showGutterIconsStatus = appSettings.showGutterIcons
67 |
68 | // Reset project settings if we have a project
69 | if (project != null) {
70 | val registry = AlpinePluginRegistry.instance
71 | registry.getRegisteredPlugins().forEach { plugin ->
72 | val pluginName = plugin.getPluginName()
73 | val enabled = registry.isPluginEnabled(project, pluginName)
74 | mySettingsComponent?.setPluginStatus(pluginName, enabled)
75 | }
76 | }
77 | }
78 |
79 | override fun disposeUIResources() {
80 | mySettingsComponent = null
81 | }
82 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | com.github.inxilpro.intellijalpine
4 |
5 | Alpine.js Support
6 |
7 |
8 | Chris Morrell
9 |
10 |
11 |
12 |
13 | com.intellij.modules.platform
14 | com.intellij.modules.lang
15 | com.intellij.modules.xml
16 | JavaScript
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
28 |
29 |
30 |
33 |
35 |
37 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow.
2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN.
3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information.
4 |
5 | name: Release
6 | on:
7 | release:
8 | types: [prereleased, released]
9 |
10 | jobs:
11 |
12 | # Prepare and publish the plugin to the Marketplace repository
13 | release:
14 | name: Publish Plugin
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 30
17 | permissions:
18 | contents: write
19 | pull-requests: write
20 | steps:
21 |
22 | # Check out current repository
23 | - name: Fetch Sources
24 | uses: actions/checkout@v4
25 | with:
26 | ref: ${{ github.event.release.tag_name }}
27 |
28 | # Setup Java environment for the next steps
29 | - name: Setup Java
30 | uses: actions/setup-java@v4
31 | with:
32 | distribution: zulu
33 | java-version: 17
34 |
35 | # Setup Gradle
36 | - name: Setup Gradle
37 | uses: gradle/gradle-build-action@v3
38 | with:
39 | gradle-home-cache-cleanup: true
40 |
41 | # Set environment variables
42 | - name: Export Properties
43 | id: properties
44 | shell: bash
45 | run: |
46 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d'
47 | ${{ github.event.release.body }}
48 | EOM
49 | )"
50 |
51 | echo "changelog<> $GITHUB_OUTPUT
52 | echo "$CHANGELOG" >> $GITHUB_OUTPUT
53 | echo "EOF" >> $GITHUB_OUTPUT
54 |
55 | # Update Unreleased section with the current release note
56 | - name: Patch Changelog
57 | if: ${{ steps.properties.outputs.changelog != '' }}
58 | env:
59 | CHANGELOG: ${{ steps.properties.outputs.changelog }}
60 | run: |
61 | ./gradlew patchChangelog --release-note="$CHANGELOG"
62 |
63 | # Publish the plugin to the Marketplace
64 | - name: Publish Plugin
65 | env:
66 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
67 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }}
68 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
69 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
70 | run: ./gradlew publishPlugin
71 |
72 | # Upload artifact as a release asset
73 | - name: Upload Release Asset
74 | env:
75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
77 |
78 | # Create pull request
79 | - name: Create Pull Request
80 | if: ${{ steps.properties.outputs.changelog != '' }}
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | run: |
84 | VERSION="${{ github.event.release.tag_name }}"
85 | BRANCH="changelog-update-$VERSION"
86 | LABEL="release changelog"
87 |
88 | git config user.email "action@github.com"
89 | git config user.name "GitHub Action"
90 |
91 | git checkout -b $BRANCH
92 | git commit -am "Changelog update - $VERSION"
93 | git push --set-upstream origin $BRANCH
94 |
95 | gh label create "$LABEL" \
96 | --description "Pull requests with release changelog update" \
97 | || true
98 |
99 | gh pr create \
100 | --title "Changelog update - \`$VERSION\`" \
101 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
102 | --label "$LABEL" \
103 | --head $BRANCH
104 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.plugins
2 |
3 | import com.github.inxilpro.intellijalpine.attributes.AttributeInfo
4 | import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions
5 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
6 | import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration
7 | import com.intellij.patterns.XmlPatterns
8 | import com.intellij.psi.xml.XmlTokenType
9 | import org.apache.commons.lang3.tuple.MutablePair
10 |
11 | class AlpineAjaxPlugin : AlpinePlugin {
12 |
13 | val targetModifiers = arrayOf(
14 | "200",
15 | "301",
16 | "302",
17 | "303",
18 | "400",
19 | "401",
20 | "403",
21 | "404",
22 | "422",
23 | "500",
24 | "502",
25 | "503",
26 | "2xx",
27 | "3xx",
28 | "4xx",
29 | "5xx",
30 | "back",
31 | "away",
32 | "replace",
33 | "push",
34 | "error",
35 | "nofocus",
36 | )
37 |
38 | override fun getPluginName(): String = "alpine-ajax"
39 |
40 | override fun getPackageDisplayName(): String = "alpine-ajax"
41 |
42 | override fun getPackageNamesForDetection(): List = listOf(
43 | "alpine-ajax",
44 | "@imacrayon/alpine-ajax"
45 | )
46 |
47 | override fun getTypeText(info: AttributeInfo): String? {
48 | if ("x-target:" == info.prefix) {
49 | return "DOM node to inject response into"
50 | }
51 |
52 | return when (info.attribute) {
53 | "x-target" -> "DOM node to inject response into"
54 | "x-headers" -> "Set AJAX request headers"
55 | "x-merge" -> "Merge response data with existing data"
56 | "x-autofocus" -> "Auto-focus on AJAX response"
57 | "x-sync" -> "Always sync on AJAX response"
58 | else -> null
59 | }
60 | }
61 |
62 | override fun injectJsContext(context: MutablePair): MutablePair {
63 | val magics = """
64 | /**
65 | * @param {string} action
66 | * @param {Object} options
67 | * @return {Promise}
68 | */
69 | function ${'$'}ajax(action, options = {}) {}
70 |
71 | """.trimIndent()
72 |
73 | return MutablePair(context.left + magics, context.right)
74 | }
75 |
76 | override fun directiveSupportJavaScript(directive: String): Boolean {
77 | return when (directive) {
78 | "x-target", "x-autofocus", "x-sync", "x-merge" -> false
79 | else -> true
80 | }
81 | }
82 |
83 | override fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {
84 | suggestions.descriptors.add(AttributeInfo("x-target:dynamic"))
85 | suggestions.addModifiers("x-target", targetModifiers)
86 | suggestions.addModifiers("x-target:dynamic", targetModifiers)
87 | }
88 |
89 | override fun getCompletionProviders(): List {
90 | return listOf(
91 | CompletionProviderRegistration(
92 | XmlPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)
93 | .withParent(
94 | XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("x-merge"))
95 | ),
96 | AlpineMergeValueCompletionProvider(this)
97 | )
98 | )
99 | }
100 |
101 | override fun getDirectives(): List = listOf(
102 | "x-target",
103 | "x-headers",
104 | "x-merge",
105 | "x-autofocus",
106 | "x-sync"
107 | )
108 |
109 | override fun getPrefixes(): List = listOf(
110 | "x-target"
111 | )
112 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.plugins
2 |
3 | import com.github.inxilpro.intellijalpine.Alpine
4 | import com.intellij.codeInsight.lookup.LookupElementBuilder
5 | import com.intellij.openapi.util.TextRange
6 | import com.intellij.patterns.XmlPatterns
7 | import com.intellij.psi.PsiElement
8 | import com.intellij.psi.PsiReference
9 | import com.intellij.psi.PsiReferenceBase
10 | import com.intellij.psi.PsiReferenceContributor
11 | import com.intellij.psi.PsiReferenceProvider
12 | import com.intellij.psi.PsiReferenceRegistrar
13 | import com.intellij.psi.util.PsiTreeUtil
14 | import com.intellij.psi.xml.XmlAttributeValue
15 | import com.intellij.psi.xml.XmlFile
16 | import com.intellij.psi.xml.XmlTag
17 | import com.intellij.util.ProcessingContext
18 |
19 | class AlpineTargetReferenceContributor : PsiReferenceContributor() {
20 | override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
21 | registrar.registerReferenceProvider(
22 | XmlPatterns.xmlAttributeValue().withParent(
23 | XmlPatterns.xmlAttribute().withName("x-target")
24 | ),
25 | AlpineTargetReferenceProvider()
26 | )
27 | }
28 | }
29 |
30 | class AlpineTargetReferenceProvider : PsiReferenceProvider() {
31 | override fun getReferencesByElement(
32 | element: PsiElement,
33 | context: ProcessingContext
34 | ): Array {
35 | val attributeValue = element as? XmlAttributeValue ?: return PsiReference.EMPTY_ARRAY
36 | val value = attributeValue.value
37 |
38 | if (value.isBlank()) return PsiReference.EMPTY_ARRAY
39 |
40 | val references = mutableListOf()
41 | val ids = value.split("\\s+".toRegex()).filter { it.isNotBlank() }
42 | var searchStart = 0
43 |
44 | for (id in ids) {
45 | val startIndex = value.indexOf(id, searchStart)
46 | if (startIndex >= 0) {
47 | // Range relative to the attribute value element (includes quotes)
48 | val range = TextRange(startIndex + 1, startIndex + id.length + 1)
49 | references.add(AlpineIdReference(attributeValue, range, id))
50 | searchStart = startIndex + id.length
51 | }
52 | }
53 |
54 | return references.toTypedArray()
55 | }
56 | }
57 |
58 | class AlpineIdReference(
59 | element: PsiElement,
60 | rangeInElement: TextRange,
61 | private val idValue: String
62 | ) : PsiReferenceBase(element, rangeInElement) {
63 |
64 | override fun resolve(): PsiElement? {
65 | val xmlFile = element.containingFile as? XmlFile ?: return null
66 | return findElementWithId(xmlFile, idValue)
67 | }
68 |
69 | override fun getVariants(): Array {
70 | val xmlFile = element.containingFile as? XmlFile ?: return emptyArray()
71 | val allIds = collectElementIds(xmlFile)
72 | val currentValue = (element as XmlAttributeValue).value
73 | val usedIds = currentValue.split("\\s+".toRegex()).filter { it.isNotBlank() }.toSet()
74 | val availableIds = allIds - usedIds
75 |
76 | return availableIds.map { id ->
77 | LookupElementBuilder.create(id)
78 | .withTypeText("Element ID")
79 | .withIcon(Alpine.ICON)
80 | }.toTypedArray()
81 | }
82 |
83 | override fun isSoft(): Boolean = false // Hard reference - should show error if unresolved
84 |
85 | private fun findElementWithId(xmlFile: XmlFile, id: String): PsiElement? {
86 | val allTags = PsiTreeUtil.findChildrenOfType(xmlFile, XmlTag::class.java)
87 |
88 | return allTags.firstOrNull { tag ->
89 | tag.getAttribute("id")?.value == id
90 | }?.getAttribute("id")?.valueElement
91 | }
92 |
93 | private fun collectElementIds(xmlFile: XmlFile): Set {
94 | val allTags = PsiTreeUtil.findChildrenOfType(xmlFile, XmlTag::class.java)
95 |
96 | return allTags.mapNotNull { tag ->
97 | tag.getAttribute("id")?.value?.takeIf { it.isNotBlank() }
98 | }.toSet()
99 | }
100 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.plugins
2 |
3 | import com.github.inxilpro.intellijalpine.attributes.AttributeInfo
4 | import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions
5 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
6 | import org.apache.commons.lang3.tuple.MutablePair
7 |
8 | class AlpineWizardPlugin : AlpinePlugin {
9 |
10 | override fun getPluginName(): String = "alpine-wizard"
11 |
12 | override fun getPackageDisplayName(): String = "alpine-wizard"
13 |
14 | override fun getPackageNamesForDetection(): List = listOf(
15 | "alpine-wizard",
16 | "@glhd/alpine-wizard"
17 | )
18 |
19 | override fun getTypeText(info: AttributeInfo): String? {
20 | if ("x-wizard:" == info.prefix) {
21 | return when (info.name) {
22 | "step" -> "Define wizard step"
23 | "if" -> "Conditional wizard step"
24 | "title" -> "Set step title"
25 | else -> "Alpine Wizard directive"
26 | }
27 | }
28 |
29 | return when (info.attribute) {
30 | "x-wizard:step" -> "Define wizard step"
31 | "x-wizard:if" -> "Conditional wizard step"
32 | "x-wizard:title" -> "Set step title"
33 | else -> null
34 | }
35 | }
36 |
37 | override fun injectJsContext(context: MutablePair): MutablePair {
38 | val wizardMagics = """
39 | class AlpineWizardStep {
40 | /** @type {HTMLElement} */ el;
41 | /** @type {string} */ title;
42 | /** @type {boolean} */ is_applicable;
43 | /** @type {boolean} */ is_complete;
44 | }
45 |
46 | class AlpineWizardProgress {
47 | /** @type {number} */ current;
48 | /** @type {number} */ total;
49 | /** @type {number} */ complete;
50 | /** @type {number} */ incomplete;
51 | /** @type {string} */ percentage;
52 | /** @type {number} */ percentage_int;
53 | /** @type {number} */ percentage_float;
54 | }
55 |
56 | class AlpineWizardMagic {
57 | /** @returns {AlpineWizardStep} */ current() {}
58 | /** @returns {AlpineWizardStep|null} */ next() {}
59 | /** @returns {AlpineWizardStep|null} */ previous() {}
60 | /** @returns {AlpineWizardProgress} */ progress() {}
61 | /** @returns {boolean} */ isFirst() {}
62 | /** @returns {boolean} */ isNotFirst() {}
63 | /** @returns {boolean} */ isLast() {}
64 | /** @returns {boolean} */ isNotLast() {}
65 | /** @returns {boolean} */ isComplete() {}
66 | /** @returns {boolean} */ isNotComplete() {}
67 | /** @returns {boolean} */ isIncomplete() {}
68 | /** @returns {boolean} */ canGoForward() {}
69 | /** @returns {boolean} */ cannotGoForward() {}
70 | /** @returns {boolean} */ canGoBack() {}
71 | /** @returns {boolean} */ cannotGoBack() {}
72 | /** @returns {void} */ forward() {}
73 | /** @returns {void} */ back() {}
74 | }
75 |
76 | /** @type {AlpineWizardMagic} */
77 | let ${'$'}wizard;
78 |
79 | """.trimIndent()
80 |
81 | return MutablePair(context.left + wizardMagics, context.right)
82 | }
83 |
84 | override fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {
85 | suggestions.descriptors.add(AttributeInfo("x-wizard:step"))
86 | suggestions.addModifiers("x-wizard:step", arrayOf("rules"))
87 |
88 | suggestions.descriptors.add(AttributeInfo("x-wizard:if"))
89 | suggestions.descriptors.add(AttributeInfo("x-wizard:title"))
90 | }
91 |
92 | override fun getDirectives(): List = listOf(
93 | "x-wizard:step",
94 | "x-wizard:if",
95 | "x-wizard:title"
96 | )
97 |
98 | override fun getPrefixes(): List = listOf(
99 | "x-wizard"
100 | )
101 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.attributes
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry
4 |
5 | @Suppress("MemberVisibilityCanBePrivate")
6 | class AttributeInfo(val attribute: String) {
7 |
8 | private val typeTexts = hashMapOf(
9 | "x-data" to "New Alpine.js component scope",
10 | "x-init" to "Run on initialization",
11 | "x-show" to "Toggles 'display: none'",
12 | "x-model" to "Add two-way binding",
13 | "x-modelable" to "Expose x-model target",
14 | "x-text" to "Bind to element's inner text",
15 | "x-html" to "Bind to element's inner HTML",
16 | "x-ref" to "Create a reference for later use",
17 | "x-if" to "Conditionally render template",
18 | "x-id" to "Register \$id() scope",
19 | "x-for" to "Map array to DOM nodes",
20 | "x-transition" to "Add transition classes",
21 | "x-transition:enter" to "Transition classes used during the entire entering phase",
22 | "x-transition:enter-start" to "Transition classes for start of entering phase",
23 | "x-transition:enter-end" to "Transition classes end of entering phase",
24 | "x-transition:leave" to "Transition classes used during the entire leaving phase",
25 | "x-transition:leave-start" to "Transition classes for start of leaving phase",
26 | "x-transition:leave-end" to "Transition classes for end of leaving phase",
27 | "x-effect" to "Add reactive effect",
28 | "x-ignore" to "Ignore DOM node in Alpine.js",
29 | "x-spread" to "Bind reusable directives",
30 | "x-cloak" to "Hide while Alpine is initializing",
31 | "x-teleport" to "Teleport template to another DOM node",
32 | "x-on" to "Add listener",
33 | "x-bind" to "Bind an attribute",
34 | "x-mask" to "Set input mask",
35 | "x-intersect" to "Bind an intersection observer",
36 | "x-trap" to "Add focus trap",
37 | "x-collapse" to "Collapse element when hidden",
38 | )
39 |
40 | val name: String
41 |
42 | val prefix: String
43 |
44 | val typeText: String
45 |
46 | init {
47 | prefix = extractPrefix()
48 | name = attribute.substring(prefix.length).substringBefore('.')
49 | typeText = buildTypeText()
50 | }
51 |
52 | @Suppress("ComplexCondition")
53 | fun isAlpine(): Boolean = isDirective() || isPrefixed() || canBePrefix()
54 |
55 | fun isEvent(): Boolean = "@" == prefix || "x-on:" == prefix
56 |
57 | fun isBound(): Boolean = ":" == prefix || "x-bind:" == prefix
58 |
59 | fun isTransition(): Boolean = "x-transition:" == prefix
60 |
61 | fun isDirective(): Boolean = AttributeUtil.directives.contains(name)
62 |
63 | fun hasValue(): Boolean = "x-cloak" != name && "x-ignore" != name
64 |
65 | fun canBePrefix(): Boolean = AttributeUtil.prefixes.contains(name)
66 |
67 | fun isPrefixed(): Boolean = prefix != ""
68 |
69 | @Suppress("ReturnCount")
70 | private fun extractPrefix(): String {
71 | for (prefix in AttributeUtil.prefixes) {
72 | if (attribute.startsWith("$prefix:")) {
73 | return "$prefix:"
74 | }
75 | }
76 |
77 | for (eventPrefix in AttributeUtil.eventPrefixes) {
78 | if (attribute.startsWith(eventPrefix)) {
79 | return eventPrefix
80 | }
81 | }
82 |
83 | for (bindPrefix in AttributeUtil.bindPrefixes) {
84 | if (attribute.startsWith(bindPrefix)) {
85 | return bindPrefix
86 | }
87 | }
88 |
89 | return ""
90 | }
91 |
92 | @Suppress("ReturnCount")
93 | private fun buildTypeText(): String {
94 | if (isEvent()) {
95 | return "'$name' listener"
96 | }
97 |
98 | if (isBound()) {
99 | return "Bind '$name' attribute"
100 | }
101 |
102 | if (isTransition()) {
103 | return "CSS classes for '$name' transition phase"
104 | }
105 |
106 | // First check plugin registry for type text
107 | val pluginTypeText = AlpinePluginRegistry.instance.getTypeText(this)
108 | if (pluginTypeText != null) {
109 | return pluginTypeText
110 | }
111 |
112 | return typeTexts.getOrDefault(attribute, "Alpine.js")
113 | }
114 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt:
--------------------------------------------------------------------------------
1 | package com.github.inxilpro.intellijalpine.core.detection
2 |
3 | import com.github.inxilpro.intellijalpine.core.AlpinePlugin
4 | import com.intellij.openapi.project.Project
5 | import com.intellij.openapi.vfs.VirtualFile
6 | import com.intellij.psi.PsiManager
7 | import com.intellij.psi.search.FilenameIndex
8 | import com.intellij.psi.search.GlobalSearchScope
9 | import com.intellij.psi.util.PsiTreeUtil
10 | import com.intellij.psi.xml.XmlFile
11 | import com.intellij.psi.xml.XmlTag
12 |
13 | class ScriptReferenceDetector : DetectionStrategy {
14 | override fun detect(project: Project, plugin: AlpinePlugin): Boolean {
15 | return hasScriptTagReferences(project, plugin) || hasImportReferences(project, plugin)
16 | }
17 |
18 | private fun hasScriptTagReferences(project: Project, plugin: AlpinePlugin): Boolean {
19 | val htmlFiles = mutableListOf()
20 | val extensions = listOf("html", "htm", "php", "twig", "djhtml", "jinja", "astro")
21 |
22 | for (extension in extensions) {
23 | htmlFiles.addAll(
24 | FilenameIndex.getAllFilesByExt(project, extension, GlobalSearchScope.projectScope(project))
25 | )
26 | }
27 |
28 | return htmlFiles.any { virtualFile ->
29 | val psiFile = PsiManager.getInstance(project).findFile(virtualFile)
30 |
31 | when {
32 | // If the IDE knows it's XML, use structured data
33 | psiFile is XmlFile -> PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java)
34 | .filter { it.name.equals("script", ignoreCase = true) }
35 | .any { scriptTag ->
36 | val src = scriptTag.getAttributeValue("src")
37 | src != null && containsPackageReference(src, plugin)
38 | }
39 |
40 | // Otherwise, just look at the contents of the file
41 | else -> {
42 | try {
43 | val content = String(virtualFile.contentsToByteArray())
44 | hasScriptTagsInContent(content, plugin)
45 | } catch (_: Exception) {
46 | false
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
53 | private fun hasImportReferences(project: Project, plugin: AlpinePlugin): Boolean {
54 | val jsExtensions = listOf("js", "ts", "mjs")
55 | val jsFiles = mutableListOf()
56 |
57 | for (extension in jsExtensions) {
58 | jsFiles.addAll(
59 | FilenameIndex.getAllFilesByExt(project, extension, GlobalSearchScope.projectScope(project))
60 | )
61 | }
62 |
63 | return jsFiles.any { virtualFile ->
64 | try {
65 | val content = String(virtualFile.contentsToByteArray())
66 | hasImportStatements(content, plugin)
67 | } catch (_: Exception) {
68 | false
69 | }
70 | }
71 | }
72 |
73 | private fun hasScriptTagsInContent(content: String, plugin: AlpinePlugin): Boolean {
74 | val scriptTagRegex = Regex("