diff --git a/.gitignore b/.gitignore index 2fa78a3..6e7ad9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle .idea .intellijPlatform +.kotlin .qodana build .DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 742b89c..93736cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,18 @@ ## [Unreleased] +### Changed +- Improved support for [alpine-wizard](https://github.com/glhd/alpine-wizard) +- Improved overall performance of plugin + ### Added +- 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 better local PhpStorm testing - Added better handling of non-HTML file types +- Added new plugin extension system ## [0.6.6] - 2025-06-19 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..acf76bb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Code Guidelines + +- Always use modern idiomatic Kotlin code +- When implementing singletons, prefer `Foo.instance` over `Foo.Companion.instance` or `Foo.getInstance()` +- Only add docblocks and comments when they provide substantive value. Comments should always explain "why" not "what." + +## Commands + +### Building & Running +- `./gradlew build` - Build the plugin +- `./gradlew buildPlugin` - Assemble plugin ZIP for deployment +- `./gradlew runIde` - Run IntelliJ IDEA with the plugin installed for testing +- `./gradlew runIdeForUiTests` - Run IDE with robot-server for UI testing + +### 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 koverReport` - Generate code coverage reports + +## Architecture + +This is an IntelliJ IDEA plugin that adds Alpine.js support. The plugin provides: + +- Auto-completion for Alpine directives (x-data, x-show, x-model, etc.) +- JavaScript language injection in Alpine attributes +- Syntax highlighting within Alpine directives +- Plugin support for third-party alpine plugins + +### Plugin Configuration + +The plugin is configured via: + +- `plugin.xml` - Main plugin manifest defining extensions and dependencies +- `gradle.properties` - Version and platform configuration +- `build.gradle.kts` - Build configuration and dependencies + +The plugin requires: + +- IntelliJ IDEA 2025.1 or newer +- JavaScript and HtmlTools plugins as dependencies +- Java 21 runtime + +### Release Process + +1. Update version in `gradle.properties` +2. Update `CHANGELOG.md` Unreleased section with version number +3. Push changes to main branch +4. Create and publish a GitHub release - this triggers automatic publishing to JetBrains Marketplace \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/Alpine.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/Alpine.kt index c1081e5..a35f0a4 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/Alpine.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/Alpine.kt @@ -5,6 +5,4 @@ import com.intellij.openapi.util.IconLoader object Alpine { @JvmField val ICON = IconLoader.getIcon("/alpineicon.svg", javaClass) - - val NAMESPACE = "Alpine.js Plugin" } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt deleted file mode 100644 index 342f881..0000000 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.inxilpro.intellijalpine - -import com.intellij.codeInsight.completion.CompletionContributor -import com.intellij.codeInsight.completion.CompletionType -import com.intellij.patterns.PlatformPatterns.psiElement -import com.intellij.patterns.XmlPatterns.xmlAttribute -import com.intellij.psi.xml.XmlTokenType - -class AlpineCompletionContributor : CompletionContributor() { - init { - extend( - CompletionType.BASIC, - psiElement(XmlTokenType.XML_NAME).withParent(xmlAttribute()), - AlpineAttributeCompletionProvider() - ) - } -} diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt deleted file mode 100644 index b54b701..0000000 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.inxilpro.intellijalpine - -import com.intellij.ui.components.JBCheckBox -import com.intellij.util.ui.FormBuilder -import javax.swing.JComponent -import javax.swing.JPanel - -class AlpineSettingsComponent { - val panel: JPanel - - private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons? ") - - val preferredFocusedComponent: JComponent - get() = myShowGutterIconsStatus - - var showGutterIconsStatus: Boolean - get() = myShowGutterIconsStatus.isSelected - set(newStatus) { - myShowGutterIconsStatus.isSelected = newStatus - } - - init { - panel = FormBuilder.createFormBuilder() - .addComponent(myShowGutterIconsStatus, 1) - .addComponentFillVertically(JPanel(), 0) - .panel - } -} diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt deleted file mode 100644 index 875096f..0000000 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.inxilpro.intellijalpine - -import com.intellij.openapi.options.Configurable -import javax.swing.JComponent - -class AlpineSettingsConfigurable : Configurable { - private var mySettingsComponent: AlpineSettingsComponent? = null - - override fun getDisplayName(): String { - return "Alpine.js" - } - - override fun getPreferredFocusedComponent(): JComponent? { - return mySettingsComponent?.preferredFocusedComponent - } - - override fun createComponent(): JComponent? { - mySettingsComponent = AlpineSettingsComponent() - return mySettingsComponent?.panel - } - - override fun isModified(): Boolean { - val settings: AlpineSettingsState = AlpineSettingsState.instance - - return mySettingsComponent?.showGutterIconsStatus != settings.showGutterIcons - } - - override fun apply() { - val settings: AlpineSettingsState = AlpineSettingsState.instance - settings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false - } - - override fun reset() { - val settings: AlpineSettingsState = AlpineSettingsState.instance - mySettingsComponent?.showGutterIconsStatus = settings.showGutterIcons - } - - override fun disposeUIResources() { - mySettingsComponent = null - } -} diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAttributeDescriptor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt similarity index 92% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAttributeDescriptor.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt index 5fd1939..c37ab8d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAttributeDescriptor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt @@ -1,5 +1,6 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.attributes +import com.github.inxilpro.intellijalpine.Alpine import com.intellij.psi.PsiElement import com.intellij.psi.meta.PsiPresentableMetaData import com.intellij.psi.xml.XmlTag @@ -46,4 +47,4 @@ class AlpineAttributeDescriptor( override fun getDefaultValue(): String? = null override fun getEnumeratedValues(): Array? = ArrayUtil.EMPTY_STRING_ARRAY -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt similarity index 70% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 018c8a8..da77b81 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -1,8 +1,11 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.attributes + +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry @Suppress("MemberVisibilityCanBePrivate") class AttributeInfo(val attribute: String) { - private val typeTexts = hashMapOf( + + private val typeTexts = hashMapOf( "x-data" to "New Alpine.js component scope", "x-init" to "Run on initialization", "x-show" to "Toggles 'display: none'", @@ -32,9 +35,6 @@ class AttributeInfo(val attribute: String) { "x-intersect" to "Bind an intersection observer", "x-trap" to "Add focus trap", "x-collapse" to "Collapse element when hidden", - "x-wizard:step" to "Add wizard step", - "x-wizard:if" to "Add wizard condition", - "x-wizard:title" to "Add title to wizard step", ) val name: String @@ -50,40 +50,30 @@ class AttributeInfo(val attribute: String) { } @Suppress("ComplexCondition") - fun isAlpine(): Boolean { - return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isWizard() - } + fun isAlpine(): Boolean = isDirective() || isPrefixed() || canBePrefix() - fun isEvent(): Boolean { - return "@" == prefix || "x-on:" == prefix - } + fun isEvent(): Boolean = "@" == prefix || "x-on:" == prefix - fun isBound(): Boolean { - return ":" == prefix || "x-bind:" == prefix - } + fun isBound(): Boolean = ":" == prefix || "x-bind:" == prefix - fun isTransition(): Boolean { - return "x-transition:" == prefix - } + fun isTransition(): Boolean = "x-transition:" == prefix - fun isDirective(): Boolean { - return AttributeUtil.directives.contains(name) - } + fun isDirective(): Boolean = AttributeUtil.directives.contains(name) - fun isWizard(): Boolean { - return "x-wizard:" == prefix - } + fun hasValue(): Boolean = "x-cloak" != name && "x-ignore" != name - fun hasValue(): Boolean { - return "x-cloak" != name && "x-ignore" != name - } + fun canBePrefix(): Boolean = AttributeUtil.prefixes.contains(name) - fun canBePrefix(): Boolean { - return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-wizard" == name - } + fun isPrefixed(): Boolean = prefix != "" @Suppress("ReturnCount") private fun extractPrefix(): String { + for (prefix in AttributeUtil.prefixes) { + if (attribute.startsWith("$prefix:")) { + return "$prefix:" + } + } + for (eventPrefix in AttributeUtil.eventPrefixes) { if (attribute.startsWith(eventPrefix)) { return eventPrefix @@ -96,14 +86,6 @@ class AttributeInfo(val attribute: String) { } } - if (attribute.startsWith("x-transition:")) { - return "x-transition:" - } - - if (attribute.startsWith("x-wizard:")) { - return "x-wizard:" - } - return "" } @@ -121,6 +103,12 @@ class AttributeInfo(val attribute: String) { return "CSS classes for '$name' transition phase" } + // First check plugin registry for type text + val pluginTypeText = AlpinePluginRegistry.instance.getTypeText(this) + if (pluginTypeText != null) { + return pluginTypeText + } + return typeTexts.getOrDefault(attribute, "Alpine.js") } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt similarity index 77% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index ff40bb7..44fd2c8 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -1,27 +1,28 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.attributes +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.github.inxilpro.intellijalpine.support.LanguageUtil +import com.intellij.openapi.project.Project import com.intellij.psi.html.HtmlTag import com.intellij.psi.impl.source.html.dtd.HtmlAttributeDescriptorImpl -import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl -import com.intellij.psi.impl.source.html.dtd.HtmlNSDescriptorImpl import com.intellij.psi.xml.XmlAttribute import com.intellij.psi.xml.XmlAttributeValue -import com.intellij.psi.xml.XmlTag -import com.intellij.xml.XmlAttributeDescriptor -import java.util.Arrays -import java.util.Collections object AttributeUtil { - private val validAttributes = mutableMapOf>() - - val xmlPrefixes = arrayOf( + private val corePrefixes = listOf( "x-on", "x-bind", "x-transition", - "x-wizard", // glhd/alpine-wizard pacakge ) - val directives = arrayOf( + val prefixes: List by lazy { + AlpinePluginRegistry.instance.getRegisteredPlugins() + .flatMap { it.getPrefixes() } + .union(corePrefixes) + .toList() + } + + private val coreDirectives = listOf( "x-data", "x-init", "x-show", @@ -46,6 +47,13 @@ object AttributeUtil { "x-spread", // deprecated ) + val directives: List by lazy { + AlpinePluginRegistry.instance.getRegisteredPlugins() + .flatMap { it.getDirectives() } + .union(coreDirectives) + .toList() + } + val templateDirectives = arrayOf( "x-if", "x-for", @@ -90,10 +98,6 @@ object AttributeUtil { "delay", ) - val numericModifiers = arrayOf( - "scale", - ) - val transitionModifiers = arrayOf( "duration", "delay", @@ -196,18 +200,24 @@ object AttributeUtil { Pair("wheel", "WheelEvent"), ) + fun getDirectivesForProject(project: Project): Array { + val pluginDirectives = AlpinePluginRegistry.instance.getAllDirectives(project) + return (directives.toList() + pluginDirectives).toTypedArray() + } + + fun getXmlPrefixesForProject(project: Project): Array { + val pluginPrefixes = AlpinePluginRegistry.instance.getAllPrefixes(project) + return (prefixes.toList() + pluginPrefixes).toTypedArray() + } + fun isXmlPrefix(prefix: String): Boolean { - return xmlPrefixes.contains(prefix) + return prefixes.contains(prefix) } fun isTemplateDirective(directive: String): Boolean { return templateDirectives.contains(directive) } - fun getValidAttributesWithInfo(xmlTag: HtmlTag): Array { - return validAttributes.getOrPut(xmlTag.name, { buildValidAttributes(xmlTag) }) - } - fun isEvent(attribute: String): Boolean { for (prefix in eventPrefixes) { if (attribute.startsWith(prefix)) { @@ -232,7 +242,7 @@ object AttributeUtil { if (!LanguageUtil.supportsAlpineJs(host.containingFile)) { return false } - + // Make sure that we have an XML attribute as a parent val attribute = host.parent as? XmlAttribute ?: return false @@ -254,7 +264,7 @@ object AttributeUtil { } // Make sure it's an attribute that is parsed as JavaScript - if (!shouldInjectJavaScript(attributeName)) { + if (!shouldInjectJavaScript(attributeName, host.containingFile.project)) { return false } @@ -279,41 +289,25 @@ object AttributeUtil { return name.startsWith("x-") || name.startsWith("@") || name.startsWith(':') } - private fun shouldInjectJavaScript(name: String): Boolean { - return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name - } - - private fun buildValidAttributes(htmlTag: HtmlTag): Array { - val descriptors = mutableListOf() + private fun shouldInjectJavaScript(name: String, project: Project): Boolean { + // Never inject for these core attributes + if (name.startsWith("x-transition:") || name == "x-mask" || name == "x-modelable") { + return false + } - for (directive in directives) { - if (htmlTag.name != "template" && isTemplateDirective(directive)) { - continue - } - descriptors.add(AttributeInfo(directive)) - } + val enabledPlugins = AlpinePluginRegistry.instance.getEnabledPlugins(project) + for (plugin in enabledPlugins) { + val pluginDirectives = plugin.getDirectives() + val pluginPrefixes = plugin.getPrefixes() - for (descriptor in getDefaultHtmlAttributes(htmlTag)) { - if (descriptor.name.startsWith("on")) { - val event = descriptor.name.substring(2) - for (prefix in eventPrefixes) { - descriptors.add(AttributeInfo(prefix + event)) - } - } else { - for (prefix in bindPrefixes) { - descriptors.add(AttributeInfo(prefix + descriptor.name)) - } + // If this attribute belongs to this plugin, let the plugin decide + if (pluginDirectives.contains(name) || pluginPrefixes.any { name.startsWith("$it:") }) { + return plugin.directiveSupportJavaScript(name) } } - return descriptors.toTypedArray() - } - - private fun getDefaultHtmlAttributes(xmlTag: XmlTag): Array { - val tagDescriptor = xmlTag.descriptor as? HtmlElementDescriptorImpl - val descriptor = tagDescriptor ?: HtmlNSDescriptorImpl.guessTagForCommonAttributes(xmlTag) - - return (descriptor as? HtmlElementDescriptorImpl)?.getDefaultAttributeDescriptors(xmlTag) ?: emptyArray() + // For core attributes and unknown attributes, default to true (inject JS) + return true } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributesProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributesProvider.kt similarity index 93% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AttributesProvider.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributesProvider.kt index 8caf55b..523dac5 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributesProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributesProvider.kt @@ -1,4 +1,4 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.attributes import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl import com.intellij.psi.xml.XmlTag @@ -21,4 +21,4 @@ class AttributesProvider : XmlAttributeDescriptorsProvider { return null } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAttributeCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt similarity index 92% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAttributeCompletionProvider.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt index 6387b61..a4b456d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAttributeCompletionProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt @@ -1,12 +1,13 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.completion +import com.github.inxilpro.intellijalpine.Alpine +import com.github.inxilpro.intellijalpine.support.LanguageUtil import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.CompletionUtilCore import com.intellij.codeInsight.completion.XmlAttributeInsertHandler import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.lang.html.HTMLLanguage import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.html.HtmlTag import com.intellij.psi.xml.XmlAttribute @@ -59,4 +60,4 @@ class AlpineAttributeCompletionProvider(vararg items: String) : CompletionProvid result.addElement(elementBuilder) } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt new file mode 100644 index 0000000..fc9615a --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt @@ -0,0 +1,30 @@ +package com.github.inxilpro.intellijalpine.completion + +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.XmlPatterns +import com.intellij.psi.xml.XmlTokenType + +class AlpineCompletionContributor : CompletionContributor() { + init { + // Attribute name completion + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement(XmlTokenType.XML_NAME).withParent(XmlPatterns.xmlAttribute()), + AlpineAttributeCompletionProvider() + ) + + // Plugin completions + AlpinePluginRegistry.instance.getRegisteredPlugins().forEach { plugin -> + plugin.getCompletionProviders().forEach { registration -> + extend( + registration.type, + registration.pattern, + registration.provider + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt new file mode 100644 index 0000000..df9b1fe --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt @@ -0,0 +1,29 @@ +package com.github.inxilpro.intellijalpine.completion + +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.util.ProcessingContext + +abstract class AlpinePluginCompletionProvider( + private val plugin: AlpinePlugin +) : CompletionProvider() { + + final override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + if (AlpinePluginRegistry.instance.isPluginEnabled(parameters.position.project, plugin)) { + addPluginCompletions(parameters, context, result) + } + } + + protected abstract fun addPluginCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt similarity index 86% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt index 87462ed..43cb9be 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt @@ -1,6 +1,8 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.completion -import com.github.inxilpro.intellijalpine.AttributeUtil.isTemplateDirective +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.attributes.AttributeUtil +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry import com.intellij.psi.html.HtmlTag import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl import com.intellij.psi.impl.source.html.dtd.HtmlNSDescriptorImpl @@ -18,12 +20,12 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String addPrefixes() addDerivedAttributes() addTransitions() - addWizard() + addPlugins() } private fun addDirectives() { - for (directive in AttributeUtil.directives) { - if (tagName != "template" && isTemplateDirective(directive)) { + for (directive in AttributeUtil.getDirectivesForProject(htmlTag.project)) { + if (tagName != "template" && AttributeUtil.isTemplateDirective(directive)) { continue } @@ -40,7 +42,7 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } private fun addPrefixes() { - for (prefix in AttributeUtil.xmlPrefixes) { + for (prefix in AttributeUtil.getXmlPrefixesForProject(htmlTag.project)) { descriptors.add(AttributeInfo(prefix)) } } @@ -77,12 +79,8 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } } - private fun addWizard() { - descriptors.add(AttributeInfo("x-wizard:step")) - addModifiers("x-wizard:step", arrayOf("rules")) - - descriptors.add(AttributeInfo("x-wizard:if")) - descriptors.add(AttributeInfo("x-wizard:title")) + private fun addPlugins() { + AlpinePluginRegistry.instance.injectAllAutoCompleteSuggestions(htmlTag.project, this) } private fun addEvent(descriptor: XmlAttributeDescriptor) { @@ -104,7 +102,7 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } } - private fun addModifiers(modifiableDirective: String, modifiers: Array) { + fun addModifiers(modifiableDirective: String, modifiers: Array) { if (!partialAttribute.startsWith(modifiableDirective)) { return } @@ -160,4 +158,4 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String return (descriptor as? HtmlElementDescriptorImpl)?.getDefaultAttributeDescriptors(htmlTag) ?: emptyArray() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoPopupHandler.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoPopupHandler.kt similarity index 94% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AutoPopupHandler.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoPopupHandler.kt index c756a84..363091a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoPopupHandler.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoPopupHandler.kt @@ -1,4 +1,4 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.completion import com.intellij.codeInsight.AutoPopupController import com.intellij.codeInsight.editorActions.TypedHandlerDelegate @@ -27,4 +27,4 @@ class AutoPopupHandler : TypedHandlerDelegate() { return Result.CONTINUE } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineLineMarkerProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt similarity index 82% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineLineMarkerProvider.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt index 276f3f7..95f10b8 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineLineMarkerProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt @@ -1,5 +1,8 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.core +import com.github.inxilpro.intellijalpine.Alpine +import com.github.inxilpro.intellijalpine.attributes.AlpineAttributeDescriptor +import com.github.inxilpro.intellijalpine.settings.AlpineSettingsState import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder @@ -31,4 +34,4 @@ class AlpineLineMarkerProvider : RelatedItemLineMarkerProvider() { result.add(builder.createLineMarkerInfo(token)) } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt new file mode 100644 index 0000000..dc42ced --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -0,0 +1,36 @@ +package com.github.inxilpro.intellijalpine.core + +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import org.apache.commons.lang3.tuple.MutablePair + +interface AlpinePlugin { + companion object { + val EP_NAME = + ExtensionPointName.Companion.create("com.github.inxilpro.intellijalpine.alpinePlugin") + } + + fun getPluginName(): String + + fun getPackageDisplayName(): String + + fun getPackageNamesForDetection(): List + + fun getTypeText(info: AttributeInfo): String? = null + + fun injectJsContext(context: MutablePair): MutablePair = context + + fun directiveSupportJavaScript(directive: String): Boolean = true + + fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {} + + fun getCompletionProviders(): List = emptyList() + + fun getDirectives(): List = emptyList() + + fun getPrefixes(): List = emptyList() + + fun performDetection(project: Project): Boolean = false +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt new file mode 100644 index 0000000..353489b --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -0,0 +1,119 @@ +package com.github.inxilpro.intellijalpine.core + +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +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.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import com.intellij.util.messages.MessageBusConnection +import org.apache.commons.lang3.tuple.MutablePair +import java.util.concurrent.ConcurrentHashMap + +@Service(Service.Level.APP) +class AlpinePluginRegistry { + private val listeners = ConcurrentHashMap() + private val detector = PluginDetector() + + companion object { + val instance: AlpinePluginRegistry + get() = ApplicationManager.getApplication().getService(AlpinePluginRegistry::class.java) + } + + fun getRegisteredPlugins(): List { + return AlpinePlugin.EP_NAME.extensionList + } + + fun getEnabledPlugins(project: Project): List { + val settings = AlpineProjectSettingsState.getInstance(project) + return getRegisteredPlugins().filter { settings.isPluginEnabled(it.getPluginName()) } + } + + fun isPluginEnabled(project: Project, pluginName: String): Boolean { + return AlpineProjectSettingsState.getInstance(project).isPluginEnabled(pluginName) + } + + fun isPluginEnabled(project: Project, plugin: AlpinePlugin): Boolean { + return isPluginEnabled(project, plugin.getPluginName()) + } + + fun enablePlugin(project: Project, pluginName: String) { + AlpineProjectSettingsState.getInstance(project).setPluginEnabled(pluginName, true) + } + + fun disablePlugin(project: Project, pluginName: String) { + AlpineProjectSettingsState.getInstance(project).setPluginEnabled(pluginName, false) + } + + fun getAllDirectives(project: Project): List { + return getEnabledPlugins(project).flatMap { it.getDirectives() } + } + + fun getAllPrefixes(project: Project): List { + return getEnabledPlugins(project).flatMap { it.getPrefixes() } + } + + fun getTypeText(info: AttributeInfo): String? { + return getRegisteredPlugins().firstNotNullOfOrNull { it.getTypeText(info) } + } + + fun injectAllJsContext(project: Project, context: MutablePair): MutablePair { + return getEnabledPlugins(project).fold(context) { acc, plugin -> + plugin.injectJsContext(acc) + } + } + + fun injectAllAutoCompleteSuggestions(project: Project, suggestions: AutoCompleteSuggestions) { + getEnabledPlugins(project).forEach { it.injectAutoCompleteSuggestions(suggestions) } + } + + fun checkAndAutoEnablePlugins(project: Project) { + getRegisteredPlugins().forEach { plugin -> + if (!isPluginEnabled(project, plugin.getPluginName()) && detector.detect(project, plugin)) { + enablePlugin(project, plugin.getPluginName()) + } + } + + setupPackageJsonListener(project) + } + + 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() + listeners[projectPath] = connection + + connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener { + override fun after(events: List) { + val hasPackageJsonChanges = events.any { event -> + val file = event.file + file != null && file.name == "package.json" + } + + if (hasPackageJsonChanges) { + ApplicationManager.getApplication().runReadAction { + getRegisteredPlugins().forEach { plugin -> + if (!isPluginEnabled(project, plugin.getPluginName()) && detector.detect(project, plugin)) { + enablePlugin(project, plugin.getPluginName()) + } + } + } + } + } + }) + } + + fun cleanup(project: Project) { + val projectPath = project.basePath ?: project.name + listeners.remove(projectPath)?.disconnect() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/CompletionProviderRegistration.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/CompletionProviderRegistration.kt new file mode 100644 index 0000000..56eb251 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/CompletionProviderRegistration.kt @@ -0,0 +1,12 @@ +package com.github.inxilpro.intellijalpine.core + +import com.github.inxilpro.intellijalpine.completion.AlpinePluginCompletionProvider +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.patterns.ElementPattern +import com.intellij.psi.PsiElement + +data class CompletionProviderRegistration( + val pattern: ElementPattern, + val provider: AlpinePluginCompletionProvider, + val type: CompletionType = CompletionType.BASIC, +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/DetectionStrategy.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/DetectionStrategy.kt new file mode 100644 index 0000000..926263f --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/DetectionStrategy.kt @@ -0,0 +1,8 @@ +package com.github.inxilpro.intellijalpine.core.detection + +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.intellij.openapi.project.Project + +interface DetectionStrategy { + fun detect(project: Project, plugin: AlpinePlugin): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PackageJsonDetector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PackageJsonDetector.kt new file mode 100644 index 0000000..23dd873 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PackageJsonDetector.kt @@ -0,0 +1,37 @@ +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.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 { + val packageJsonFiles = FilenameIndex.getVirtualFilesByName( + "package.json", + GlobalSearchScope.projectScope(project) + ) + + 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 + + hasPluginDependency(plugin, dependencies) || hasPluginDependency(plugin, devDependencies) + } else { + false + } + } + } + + private fun hasPluginDependency(plugin: AlpinePlugin, dependencies: JsonObject?): Boolean { + return plugin.getPackageNamesForDetection().any { packageName -> + dependencies?.findProperty(packageName) != null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt new file mode 100644 index 0000000..5f0bc4f --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt @@ -0,0 +1,15 @@ +package com.github.inxilpro.intellijalpine.core.detection + +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.intellij.openapi.project.Project + +class PluginDetector : DetectionStrategy { + private val strategies: List = listOf( + PackageJsonDetector(), + ScriptReferenceDetector() + ) + + override fun detect(project: Project, plugin: AlpinePlugin): Boolean { + return strategies.any { it.detect(project, plugin) } || plugin.performDetection(project) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt new file mode 100644 index 0000000..f13410f --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt @@ -0,0 +1,102 @@ +package com.github.inxilpro.intellijalpine.core.detection + +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiManager +import com.intellij.psi.search.FilenameIndex +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag + +class ScriptReferenceDetector : DetectionStrategy { + override fun detect(project: Project, plugin: AlpinePlugin): Boolean { + return hasScriptTagReferences(project, plugin) || hasImportReferences(project, plugin) + } + + private fun hasScriptTagReferences(project: Project, plugin: AlpinePlugin): Boolean { + val htmlFiles = mutableListOf() + val extensions = listOf("html", "htm", "php", "twig", "djhtml", "jinja", "astro") + + for (extension in extensions) { + htmlFiles.addAll( + FilenameIndex.getAllFilesByExt(project, extension, GlobalSearchScope.projectScope(project)) + ) + } + + return htmlFiles.any { virtualFile -> + val psiFile = PsiManager.getInstance(project).findFile(virtualFile) + + when { + // If the IDE knows it's XML, use structured data + psiFile is XmlFile -> PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) + .filter { it.name.equals("script", ignoreCase = true) } + .any { scriptTag -> + val src = scriptTag.getAttributeValue("src") + src != null && containsPackageReference(src, plugin) + } + + // Otherwise, just look at the contents of the file + else -> { + try { + val content = String(virtualFile.contentsToByteArray()) + hasScriptTagsInContent(content, plugin) + } catch (_: Exception) { + false + } + } + } + } + } + + private fun hasImportReferences(project: Project, plugin: AlpinePlugin): Boolean { + val jsExtensions = listOf("js", "ts", "mjs") + val jsFiles = mutableListOf() + + for (extension in jsExtensions) { + jsFiles.addAll( + FilenameIndex.getAllFilesByExt(project, extension, GlobalSearchScope.projectScope(project)) + ) + } + + return jsFiles.any { virtualFile -> + try { + val content = String(virtualFile.contentsToByteArray()) + hasImportStatements(content, plugin) + } catch (_: Exception) { + false + } + } + } + + private fun hasScriptTagsInContent(content: String, plugin: AlpinePlugin): Boolean { + val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) + return scriptTagRegex.findAll(content).any { match -> + val src = match.groupValues[1] + containsPackageReference(src, plugin) + } + } + + private fun hasImportStatements(content: String, plugin: AlpinePlugin): Boolean { + val importPatterns = plugin.getPackageNamesForDetection().flatMap { packageName -> + val escapedPackageName = Regex.escape(packageName) + listOf( + "import\\s+.*\\s+from\\s+['\"]$escapedPackageName['\"]", + "import\\s+['\"]$escapedPackageName['\"]", + "require\\s*\\(\\s*['\"]$escapedPackageName['\"]\\s*\\)", + "from\\s+['\"]$escapedPackageName['\"]\\s+import" + ) + } + + return importPatterns.any { pattern -> + Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) + } + } + + private fun containsPackageReference(src: String, plugin: AlpinePlugin): Boolean { + return plugin.getPackageNamesForDetection().any { packageName -> + src.contains(packageName, ignoreCase = true) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt similarity index 58% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt index 4b2d7f7..bda505d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt @@ -1,8 +1,11 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.injection +import com.github.inxilpro.intellijalpine.attributes.AttributeUtil +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.github.inxilpro.intellijalpine.support.LanguageUtil +import com.intellij.lang.Language import com.intellij.lang.injection.MultiHostInjector import com.intellij.lang.injection.MultiHostRegistrar -import com.intellij.lang.Language import com.intellij.openapi.util.TextRange import com.intellij.psi.ElementManipulators import com.intellij.psi.PsiElement @@ -12,123 +15,75 @@ import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.xml.XmlAttribute import com.intellij.psi.xml.XmlAttributeValue import com.intellij.psi.xml.XmlTag -import com.intellij.refactoring.suggested.startOffset import org.apache.commons.lang3.tuple.MutablePair import org.apache.html.dom.HTMLDocumentImpl -import java.util.* class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { - private companion object { - val globalState = - """ - /** @type {Object.} */ - let ${'$'}refs; - - /** @type {Object.} */ - let ${'$'}store; - - """.trimIndent() - - val alpineWizardState = - """ - class AlpineWizardStep { - /** @type {HTMLElement} */ el; - /** @type {string} */ title; - /** @type {boolean} */ is_applicable; - /** @type {boolean} */ is_complete; - } - - class AlpineWizardProgress { - /** @type {number} */ current; - /** @type {number} */ total; - /** @type {number} */ complete; - /** @type {number} */ incomplete; - /** @type {string} */ percentage; - /** @type {number} */ percentage_int; - /** @type {number} */ percentage_float; - } - - class AlpineWizardMagic { - /** @returns {AlpineWizardStep} */ current() {} - /** @returns {AlpineWizardStep|null} */ next() {} - /** @returns {AlpineWizardStep|null} */ previous() {} - /** @returns {AlpineWizardProgress} */ progress() {} - /** @returns {boolean} */ isFirst() {} - /** @returns {boolean} */ isNotFirst() {} - /** @returns {boolean} */ isLast() {} - /** @returns {boolean} */ isNotLast() {} - /** @returns {boolean} */ isComplete() {} - /** @returns {boolean} */ isNotComplete() {} - /** @returns {boolean} */ isIncomplete() {} - /** @returns {boolean} */ canGoForward() {} - /** @returns {boolean} */ cannotGoForward() {} - /** @returns {boolean} */ canGoBack() {} - /** @returns {boolean} */ cannotGoBack() {} - /** @returns {void} */ forward() {} - /** @returns {void} */ back() {} - } - - /** @type {AlpineWizardMagic} */ - let ${'$'}wizard; - - """.trimIndent() - - val globalMagics = - """ - /** - * @param {*} value - * @return {ValueToPersist} - * @template ValueToPersist - */ - function ${'$'}persist(value) {} - - /** - * @param {*} value - * @return {ValueForQueryString} - * @template ValueForQueryString - */ - function ${'$'}queryString(value) {} - - """.trimIndent() - - val coreMagics = - """ - /** @type {elType} */ - let ${'$'}el; - - /** @type {rootType} */ - let ${'$'}root; - - /** - * @param {string} event - * @param {Object} detail - * @return boolean - */ - function ${'$'}dispatch(event, detail = {}) {} - - /** - * @param {Function} callback - * @return void - */ - function ${'$'}nextTick(callback) {} - - /** - * @param {string} property - * @param {Function} callback - * @return void - */ - function ${'$'}watch(property, callback) {} - - /** - * @param {string} scope - * @return string - */ - function ${'$'}id(scope) {} - - """.trimIndent() - - val eventMagics = "/** @type {eventType} */\nlet ${'$'}event;\n\n" - } + private val globalState = + """ + /** @type {Object.} */ + let ${'$'}refs; + + /** @type {Object.} */ + let ${'$'}store; + + """.trimIndent() + + private val globalMagics = + """ + /** + * @param {*} value + * @return {ValueToPersist} + * @template ValueToPersist + */ + function ${'$'}persist(value) {} + + /** + * @param {*} value + * @return {ValueForQueryString} + * @template ValueForQueryString + */ + function ${'$'}queryString(value) {} + + """.trimIndent() + + private val coreMagics = + """ + /** @type {elType} */ + let ${'$'}el; + + /** @type {rootType} */ + let ${'$'}root; + + /** + * @param {string} event + * @param {Object} detail + * @return boolean + */ + function ${'$'}dispatch(event, detail = {}) {} + + /** + * @param {Function} callback + * @return void + */ + function ${'$'}nextTick(callback) {} + + /** + * @param {string} property + * @param {Function} callback + * @return void + */ + function ${'$'}watch(property, callback) {} + + /** + * @param {string} scope + * @return string + */ + function ${'$'}id(scope) {} + + """.trimIndent() + + private val eventMagics = "/** @type {eventType} */\nlet ${'$'}event;\n\n" override fun getLanguagesToInject(registrar: MultiHostRegistrar, host: PsiElement) { if (host !is XmlAttributeValue) { @@ -146,7 +101,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { var (prefix, suffix) = getPrefixAndSuffix(attributeName, host) - val jsLanguage = Language.findLanguageByID("JavaScript") + val jsLanguage = Language.findLanguageByID("JavaScript") ?: throw IllegalStateException("JavaScript language not found") registrar.startInjecting(jsLanguage) @@ -177,7 +132,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { return listOf(valueRange) } - val phpMatcher = Regex("(?:(?|@[a-zA-Z]+\\(.*\\)(?:\\.defer)?)") + val phpMatcher = Regex("(?|@[a-zA-Z]+\\(.*\\)(?:\\.defer)?") val ranges = mutableListOf() var offset = valueRange.startOffset @@ -192,7 +147,8 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { } private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { - val context = MutablePair(globalMagics, "") + val globalContext = MutablePair(globalMagics, "") + val context = AlpinePluginRegistry.instance.injectAllJsContext(host.project, globalContext) if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left @@ -243,7 +199,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { val data = dataParent.getAttribute("x-data")?.value if (null != data) { val (prefix, suffix) = context - context.left = "$globalState\n$alpineWizardState\nlet ${'$'}data = $data;\nwith (${'$'}data) {\n\n$prefix" + context.left = "$globalState\nlet ${'$'}data = $data;\nwith (${'$'}data) {\n\n$prefix" context.right = "$suffix\n\n}" } } @@ -288,4 +244,4 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { val eventName = AttributeUtil.getEventNameFromDirective(directive) return eventMagics.replace("eventType", eventName) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt new file mode 100644 index 0000000..062d454 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -0,0 +1,112 @@ +package com.github.inxilpro.intellijalpine.plugins + +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration +import com.intellij.patterns.XmlPatterns +import com.intellij.psi.xml.XmlTokenType +import org.apache.commons.lang3.tuple.MutablePair + +class AlpineAjaxPlugin : AlpinePlugin { + + val targetModifiers = arrayOf( + "200", + "301", + "302", + "303", + "400", + "401", + "403", + "404", + "422", + "500", + "502", + "503", + "2xx", + "3xx", + "4xx", + "5xx", + "back", + "away", + "replace", + "push", + "error", + "nofocus", + ) + + override fun getPluginName(): String = "alpine-ajax" + + override fun getPackageDisplayName(): String = "alpine-ajax" + + override fun getPackageNamesForDetection(): List = listOf( + "alpine-ajax", + "@imacrayon/alpine-ajax" + ) + + override fun getTypeText(info: AttributeInfo): String? { + if ("x-target:" == info.prefix) { + return "DOM node to inject response into" + } + + return when (info.attribute) { + "x-target" -> "DOM node to inject response into" + "x-headers" -> "Set AJAX request headers" + "x-merge" -> "Merge response data with existing data" + "x-autofocus" -> "Auto-focus on AJAX response" + "x-sync" -> "Always sync on AJAX response" + else -> null + } + } + + override fun injectJsContext(context: MutablePair): MutablePair { + val magics = """ + /** + * @param {string} action + * @param {Object} options + * @return {Promise} + */ + function ${'$'}ajax(action, options = {}) {} + + """.trimIndent() + + return MutablePair(context.left + magics, context.right) + } + + override fun directiveSupportJavaScript(directive: String): Boolean { + return when (directive) { + "x-target", "x-autofocus", "x-sync", "x-merge" -> false + else -> true + } + } + + override fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) { + suggestions.descriptors.add(AttributeInfo("x-target:dynamic")) + suggestions.addModifiers("x-target", targetModifiers) + suggestions.addModifiers("x-target:dynamic", targetModifiers) + } + + override fun getCompletionProviders(): List { + return listOf( + CompletionProviderRegistration( + XmlPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) + .withParent( + XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("x-merge")) + ), + AlpineMergeValueCompletionProvider(this) + ) + ) + } + + override fun getDirectives(): List = listOf( + "x-target", + "x-headers", + "x-merge", + "x-autofocus", + "x-sync" + ) + + override fun getPrefixes(): List = listOf( + "x-target" + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt new file mode 100644 index 0000000..728bf5a --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt @@ -0,0 +1,48 @@ +package com.github.inxilpro.intellijalpine.plugins + +import com.github.inxilpro.intellijalpine.Alpine +import com.github.inxilpro.intellijalpine.completion.AlpinePluginCompletionProvider +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.xml.XmlAttribute +import com.intellij.util.ProcessingContext + +class AlpineMergeValueCompletionProvider( + plugin: AlpinePlugin +) : AlpinePluginCompletionProvider(plugin) { + + private val mergeStrategies = arrayOf( + "before" to "Insert content before target", + "replace" to "Replace target element (default)", + "update" to "Update target's innerHTML", + "prepend" to "Prepend content to target", + "append" to "Append content to target", + "after" to "Insert content after target", + "morph" to "Morph content preserving state" + ) + + override fun addPluginCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val element = parameters.position + val attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute::class.java) ?: return + + if (attribute.name != "x-merge") { + return + } + + for ((strategy, description) in mergeStrategies) { + val lookupElement = LookupElementBuilder + .create(strategy) + .withTypeText(description) + .withIcon(Alpine.ICON) + + result.addElement(lookupElement) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt new file mode 100644 index 0000000..514eccd --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt @@ -0,0 +1,100 @@ +package com.github.inxilpro.intellijalpine.plugins + +import com.github.inxilpro.intellijalpine.Alpine +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.TextRange +import com.intellij.patterns.XmlPatterns +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReference +import com.intellij.psi.PsiReferenceBase +import com.intellij.psi.PsiReferenceContributor +import com.intellij.psi.PsiReferenceProvider +import com.intellij.psi.PsiReferenceRegistrar +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.xml.XmlAttributeValue +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import com.intellij.util.ProcessingContext + +class AlpineTargetReferenceContributor : PsiReferenceContributor() { + override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { + registrar.registerReferenceProvider( + XmlPatterns.xmlAttributeValue().withParent( + XmlPatterns.xmlAttribute().withName("x-target") + ), + AlpineTargetReferenceProvider() + ) + } +} + +class AlpineTargetReferenceProvider : PsiReferenceProvider() { + override fun getReferencesByElement( + element: PsiElement, + context: ProcessingContext + ): Array { + val attributeValue = element as? XmlAttributeValue ?: return PsiReference.EMPTY_ARRAY + val value = attributeValue.value + + if (value.isBlank()) return PsiReference.EMPTY_ARRAY + + val references = mutableListOf() + val ids = value.split("\\s+".toRegex()).filter { it.isNotBlank() } + var searchStart = 0 + + for (id in ids) { + val startIndex = value.indexOf(id, searchStart) + if (startIndex >= 0) { + // Range relative to the attribute value element (includes quotes) + val range = TextRange(startIndex + 1, startIndex + id.length + 1) + references.add(AlpineIdReference(attributeValue, range, id)) + searchStart = startIndex + id.length + } + } + + return references.toTypedArray() + } +} + +class AlpineIdReference( + element: PsiElement, + rangeInElement: TextRange, + private val idValue: String +) : PsiReferenceBase(element, rangeInElement) { + + override fun resolve(): PsiElement? { + val xmlFile = element.containingFile as? XmlFile ?: return null + return findElementWithId(xmlFile, idValue) + } + + override fun getVariants(): Array { + val xmlFile = element.containingFile as? XmlFile ?: return emptyArray() + val allIds = collectElementIds(xmlFile) + val currentValue = (element as XmlAttributeValue).value + val usedIds = currentValue.split("\\s+".toRegex()).filter { it.isNotBlank() }.toSet() + val availableIds = allIds - usedIds + + return availableIds.map { id -> + LookupElementBuilder.create(id) + .withTypeText("Element ID") + .withIcon(Alpine.ICON) + }.toTypedArray() + } + + override fun isSoft(): Boolean = false // Hard reference - should show error if unresolved + + private fun findElementWithId(xmlFile: XmlFile, id: String): PsiElement? { + val allTags = PsiTreeUtil.findChildrenOfType(xmlFile, XmlTag::class.java) + + return allTags.firstOrNull { tag -> + tag.getAttribute("id")?.value == id + }?.getAttribute("id")?.valueElement + } + + private fun collectElementIds(xmlFile: XmlFile): Set { + val allTags = PsiTreeUtil.findChildrenOfType(xmlFile, XmlTag::class.java) + + return allTags.mapNotNull { tag -> + tag.getAttribute("id")?.value?.takeIf { it.isNotBlank() } + }.toSet() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt new file mode 100644 index 0000000..06d4b8e --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt @@ -0,0 +1,101 @@ +package com.github.inxilpro.intellijalpine.plugins + +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import org.apache.commons.lang3.tuple.MutablePair + +class AlpineWizardPlugin : AlpinePlugin { + + override fun getPluginName(): String = "alpine-wizard" + + override fun getPackageDisplayName(): String = "alpine-wizard" + + override fun getPackageNamesForDetection(): List = listOf( + "alpine-wizard", + "@glhd/alpine-wizard" + ) + + override fun getTypeText(info: AttributeInfo): String? { + if ("x-wizard:" == info.prefix) { + return when (info.name) { + "step" -> "Define wizard step" + "if" -> "Conditional wizard step" + "title" -> "Set step title" + else -> "Alpine Wizard directive" + } + } + + return when (info.attribute) { + "x-wizard:step" -> "Define wizard step" + "x-wizard:if" -> "Conditional wizard step" + "x-wizard:title" -> "Set step title" + else -> null + } + } + + override fun injectJsContext(context: MutablePair): MutablePair { + val wizardMagics = """ + class AlpineWizardStep { + /** @type {HTMLElement} */ el; + /** @type {string} */ title; + /** @type {boolean} */ is_applicable; + /** @type {boolean} */ is_complete; + } + + class AlpineWizardProgress { + /** @type {number} */ current; + /** @type {number} */ total; + /** @type {number} */ complete; + /** @type {number} */ incomplete; + /** @type {string} */ percentage; + /** @type {number} */ percentage_int; + /** @type {number} */ percentage_float; + } + + class AlpineWizardMagic { + /** @returns {AlpineWizardStep} */ current() {} + /** @returns {AlpineWizardStep|null} */ next() {} + /** @returns {AlpineWizardStep|null} */ previous() {} + /** @returns {AlpineWizardProgress} */ progress() {} + /** @returns {boolean} */ isFirst() {} + /** @returns {boolean} */ isNotFirst() {} + /** @returns {boolean} */ isLast() {} + /** @returns {boolean} */ isNotLast() {} + /** @returns {boolean} */ isComplete() {} + /** @returns {boolean} */ isNotComplete() {} + /** @returns {boolean} */ isIncomplete() {} + /** @returns {boolean} */ canGoForward() {} + /** @returns {boolean} */ cannotGoForward() {} + /** @returns {boolean} */ canGoBack() {} + /** @returns {boolean} */ cannotGoBack() {} + /** @returns {void} */ forward() {} + /** @returns {void} */ back() {} + } + + /** @type {AlpineWizardMagic} */ + let ${'$'}wizard; + + """.trimIndent() + + return MutablePair(context.left + wizardMagics, context.right) + } + + override fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) { + suggestions.descriptors.add(AttributeInfo("x-wizard:step")) + suggestions.addModifiers("x-wizard:step", arrayOf("rules")) + + suggestions.descriptors.add(AttributeInfo("x-wizard:if")) + suggestions.descriptors.add(AttributeInfo("x-wizard:title")) + } + + override fun getDirectives(): List = listOf( + "x-wizard:step", + "x-wizard:if", + "x-wizard:title" + ) + + override fun getPrefixes(): List = listOf( + "x-wizard" + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt new file mode 100644 index 0000000..5cce162 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt @@ -0,0 +1,64 @@ +package com.github.inxilpro.intellijalpine.plugins + +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import org.apache.commons.lang3.tuple.MutablePair + +class TooltipPlugin : AlpinePlugin { + + override fun getPluginName(): String = "alpine-tooltip" + + override fun getPackageDisplayName(): String = "alpine-tooltip" + + override fun getPackageNamesForDetection(): List = listOf( + "alpine-tooltip", + "@ryangjchandler/alpine-tooltip" + ) + + override fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) { + val modifiers = arrayOf( + "duration", + "delay", + "cursor", + "on", + "arrowless", + "html", + "interactive", + "border", + "debounce", + "max-width", + "theme", + "placement", + "animation", + "no-flip", + ) + + suggestions.addModifiers("x-tooltip", modifiers) + } + + override fun getTypeText(info: AttributeInfo): String? { + return when (info.attribute) { + "x-tooltip" -> "Tippy.js tooltip" + else -> null + } + } + + override fun injectJsContext(context: MutablePair): MutablePair { + val magics = """ + /** + * @param {string} value + * @param {Object} options + * @return {Promise} + */ + function ${'$'}tooltip(value, options = {}) {} + + """.trimIndent() + + return MutablePair(context.left + magics, context.right) + } + + override fun getDirectives(): List = listOf( + "x-tooltip", + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt new file mode 100644 index 0000000..22d6456 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt @@ -0,0 +1,14 @@ +package com.github.inxilpro.intellijalpine.settings + +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.intellij.openapi.application.readAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity + +class AlpineProjectActivity : ProjectActivity { + override suspend fun execute(project: Project) { + readAction { + AlpinePluginRegistry.instance.checkAndAutoEnablePlugins(project) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt new file mode 100644 index 0000000..0d013f3 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt @@ -0,0 +1,11 @@ +package com.github.inxilpro.intellijalpine.settings + +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManagerListener + +class AlpineProjectListener : ProjectManagerListener { + override fun projectClosed(project: Project) { + AlpinePluginRegistry.instance.cleanup(project) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt new file mode 100644 index 0000000..ac3331d --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt @@ -0,0 +1,37 @@ +package com.github.inxilpro.intellijalpine.settings + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.XmlSerializerUtil + +@State( + name = "com.github.inxilpro.intellijalpine.AlpineProjectSettingsState", + storages = [Storage("alpinejs-support.xml")] +) +class AlpineProjectSettingsState : PersistentStateComponent { + var enabledPlugins = mutableMapOf() + + fun isPluginEnabled(pluginName: String): Boolean { + return enabledPlugins[pluginName] ?: false + } + + fun setPluginEnabled(pluginName: String, enabled: Boolean) { + enabledPlugins[pluginName] = enabled + } + + override fun getState(): AlpineProjectSettingsState? { + return this + } + + override fun loadState(state: AlpineProjectSettingsState) { + XmlSerializerUtil.copyBean(state, this) + } + + companion object { + fun getInstance(project: Project): AlpineProjectSettingsState { + return project.getService(AlpineProjectSettingsState::class.java) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt new file mode 100644 index 0000000..8446fed --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -0,0 +1,62 @@ +package com.github.inxilpro.intellijalpine.settings + +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.intellij.openapi.project.Project +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.UIUtil +import javax.swing.JComponent +import javax.swing.JPanel + +class AlpineSettingsComponent(project: Project?) { + val panel: JPanel + + private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons") + private val pluginCheckBoxes = mutableMapOf() + + val preferredFocusedComponent: JComponent + get() = myShowGutterIconsStatus + + var showGutterIconsStatus: Boolean + get() = myShowGutterIconsStatus.isSelected + set(newStatus) { + myShowGutterIconsStatus.isSelected = newStatus + } + + fun getPluginStatus(pluginName: String): Boolean { + return pluginCheckBoxes[pluginName]?.isSelected ?: false + } + + fun setPluginStatus(pluginName: String, enabled: Boolean) { + pluginCheckBoxes[pluginName]?.isSelected = enabled + } + + init { + val builder = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator("Plugin Settings")) + .addComponent(myShowGutterIconsStatus, 1) + + // Only show project settings if we have a project context + if (project != null) { + builder.addVerticalGap(10) // Add spacing between sections + .addComponent(TitledSeparator("Project Settings for “${project.name}”")) + + val projectLabel = JBLabel("These settings apply only to the current project") + projectLabel.foreground = UIUtil.getContextHelpForeground() + projectLabel.font = UIUtil.getLabelFont(UIUtil.FontSize.SMALL) + builder.addComponent(projectLabel, 1) + .addVerticalGap(5) + + // Dynamically add checkboxes for each registered plugin + AlpinePluginRegistry.instance.getRegisteredPlugins().forEach { plugin -> + val checkBox = JBCheckBox("Enable “${plugin.getPackageDisplayName()}” support for this project") + pluginCheckBoxes[plugin.getPluginName()] = checkBox + builder.addComponent(checkBox, 1) + } + } + + panel = builder.addComponentFillVertically(JPanel(), 0).panel + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt new file mode 100644 index 0000000..c93a64b --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt @@ -0,0 +1,82 @@ +package com.github.inxilpro.intellijalpine.settings + +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import javax.swing.JComponent + +class AlpineSettingsConfigurable(private val project: Project?) : Configurable { + private var mySettingsComponent: AlpineSettingsComponent? = null + + @Suppress("DialogTitleCapitalization") + override fun getDisplayName(): String { + return "Alpine.js" + } + + override fun getPreferredFocusedComponent(): JComponent? { + return mySettingsComponent?.preferredFocusedComponent + } + + override fun createComponent(): JComponent? { + mySettingsComponent = AlpineSettingsComponent(project) + return mySettingsComponent?.panel + } + + override fun isModified(): Boolean { + val appSettings = AlpineSettingsState.instance + var isModified = mySettingsComponent?.showGutterIconsStatus != appSettings.showGutterIcons + + // Check project settings if we have a project + if (project != null) { + val registry = AlpinePluginRegistry.instance + registry.getRegisteredPlugins().forEach { plugin -> + val pluginName = plugin.getPluginName() + val currentStatus = mySettingsComponent?.getPluginStatus(pluginName) ?: false + val savedStatus = registry.isPluginEnabled(project, pluginName) + if (currentStatus != savedStatus) { + isModified = true + } + } + } + + return isModified + } + + override fun apply() { + val appSettings = AlpineSettingsState.instance + appSettings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false + + // Apply project settings if we have a project + if (project != null) { + val registry = AlpinePluginRegistry.instance + registry.getRegisteredPlugins().forEach { plugin -> + val pluginName = plugin.getPluginName() + val enabled = mySettingsComponent?.getPluginStatus(pluginName) ?: false + if (enabled) { + registry.enablePlugin(project, pluginName) + } else { + registry.disablePlugin(project, pluginName) + } + } + } + } + + override fun reset() { + val appSettings = AlpineSettingsState.instance + mySettingsComponent?.showGutterIconsStatus = appSettings.showGutterIcons + + // Reset project settings if we have a project + if (project != null) { + val registry = AlpinePluginRegistry.instance + registry.getRegisteredPlugins().forEach { plugin -> + val pluginName = plugin.getPluginName() + val enabled = registry.isPluginEnabled(project, pluginName) + mySettingsComponent?.setPluginStatus(pluginName, enabled) + } + } + } + + override fun disposeUIResources() { + mySettingsComponent = null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsState.kt similarity index 94% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsState.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsState.kt index 39bdfdb..9b4adfb 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsState.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsState.kt @@ -1,4 +1,4 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.settings import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.PersistentStateComponent @@ -22,4 +22,4 @@ class AlpineSettingsState : PersistentStateComponent { val instance: AlpineSettingsState get() = ApplicationManager.getApplication().getService(AlpineSettingsState::class.java) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/LanguageUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt similarity index 89% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/LanguageUtil.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt index d1bb2b3..55b2e38 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/LanguageUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt @@ -1,10 +1,8 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.support import com.intellij.lang.Language import com.intellij.lang.html.HTMLLanguage import com.intellij.lang.xml.XMLLanguage -import com.intellij.openapi.fileTypes.FileType -import com.intellij.openapi.fileTypes.LanguageFileType import com.intellij.psi.PsiFile import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider @@ -22,7 +20,9 @@ object LanguageUtil { ) fun supportsAlpineJs(file: PsiFile): Boolean { - return hasHtmlBasedLanguage(file) || hasTemplateLanguage(file) || hasHtmlLikeExtension(file) || isTemplateLanguageFile(file); + return hasHtmlBasedLanguage(file) || hasTemplateLanguage(file) || hasHtmlLikeExtension(file) || isTemplateLanguageFile( + file + ) } fun hasPhpLanguage(file: PsiFile): Boolean { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/XmlExtension.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt similarity index 87% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/XmlExtension.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt index 99735f9..03fd7e6 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/XmlExtension.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt @@ -1,6 +1,6 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.support -import com.intellij.lang.html.HTMLLanguage +import com.github.inxilpro.intellijalpine.attributes.AttributeUtil import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile import com.intellij.psi.html.HtmlTag @@ -9,7 +9,7 @@ import com.intellij.psi.xml.XmlTag import com.intellij.xml.HtmlXmlExtension class XmlExtension : HtmlXmlExtension() { - override fun isAvailable(file: PsiFile?): Boolean { + override fun isAvailable(file: PsiFile?): Boolean { if (file == null) return false return LanguageUtil.supportsAlpineJs(file) } @@ -32,4 +32,4 @@ class XmlExtension : HtmlXmlExtension() { .find { it.name.startsWith(namespacePrefix) } ?.let { SchemaPrefix(it, TextRange.create(0, namespacePrefix.length), "Alpine.js") } } -} +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 02d2513..de67665 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -15,20 +15,42 @@ com.intellij.modules.xml JavaScript + + + + - - - - + + + + + - - + implementationClass="com.github.inxilpro.intellijalpine.core.AlpineLineMarkerProvider"/> + + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg index 78ab3a6..68a2bbe 100644 --- a/src/main/resources/META-INF/pluginIcon.svg +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -1,5 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/src/main/resources/alpineicon.svg b/src/main/resources/alpineicon.svg index 78ab3a6..68a2bbe 100644 --- a/src/main/resources/alpineicon.svg +++ b/src/main/resources/alpineicon.svg @@ -1,5 +1,6 @@ - - - + + + \ No newline at end of file