From 8710b7e8b7a8765c7e75e1bc07b57a981cf6fa97 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 18 Jun 2025 20:28:00 -0400 Subject: [PATCH 01/32] WIP --- CLAUDE.md | 60 +++++++++++++ .../AlpineCompletionContributor.kt | 10 +++ .../AlpineJavaScriptAttributeValueInjector.kt | 51 +++++++++++ ...eTargetAttributeValueCompletionProvider.kt | 90 +++++++++++++++++++ .../inxilpro/intellijalpine/AttributeInfo.kt | 32 ++++++- .../inxilpro/intellijalpine/AttributeUtil.kt | 25 +++++- .../intellijalpine/AutoCompleteSuggestions.kt | 18 ++++ 7 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..47e6e04 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 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 + +### Publishing +- `./gradlew patchChangelog` - Update changelog +- `./gradlew signPlugin` - Sign the plugin ZIP +- `./gradlew publishPlugin` - Publish to JetBrains Marketplace (requires token) + +## 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 + +### Key Components + +1. **AttributesProvider** - Central class that provides Alpine attribute descriptors to the IDE's HTML/XML support system. It defines which Alpine attributes are available and their properties. + +2. **AlpineJavaScriptAttributeValueInjector** - Injects JavaScript language into Alpine attribute values, enabling proper syntax highlighting and code completion within attributes like `x-data` and `x-show`. + +3. **AlpineCompletionContributor** - Handles auto-completion logic for Alpine directives, providing context-aware suggestions based on cursor position. + +4. **AttributeInfo** - Contains all Alpine directive definitions and metadata, including documentation and allowed contexts for each directive. + +### 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 2023.1 or newer +- JavaScript and HtmlTools plugins as dependencies +- Java 17 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/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt index 342f881..ef6516e 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt @@ -4,14 +4,24 @@ 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.patterns.XmlPatterns.xmlAttributeValue import com.intellij.psi.xml.XmlTokenType class AlpineCompletionContributor : CompletionContributor() { init { + // Attribute name completion extend( CompletionType.BASIC, psiElement(XmlTokenType.XML_NAME).withParent(xmlAttribute()), AlpineAttributeCompletionProvider() ) + + // Attribute value completion for x-target + extend( + CompletionType.BASIC, + psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) + .inside(xmlAttributeValue().withParent(xmlAttribute().withName("x-target"))), + AlpineTargetAttributeValueCompletionProvider() + ) } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt index cef5d00..8ce2530 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt @@ -89,6 +89,57 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { */ function ${'$'}queryString(value) {} + class AlpineAjaxMagic { + /** + * @param {string} url + * @param {Object} options + * @return {Promise} + */ + get(url, options = {}) {} + + /** + * @param {string} url + * @param {Object} data + * @param {Object} options + * @return {Promise} + */ + post(url, data = {}, options = {}) {} + + /** + * @param {string} url + * @param {Object} data + * @param {Object} options + * @return {Promise} + */ + put(url, data = {}, options = {}) {} + + /** + * @param {string} url + * @param {Object} data + * @param {Object} options + * @return {Promise} + */ + patch(url, data = {}, options = {}) {} + + /** + * @param {string} url + * @param {Object} options + * @return {Promise} + */ + delete(url, options = {}) {} + + /** + * @param {string} url + * @param {FormData} formData + * @param {Object} options + * @return {Promise} + */ + submit(url, formData, options = {}) {} + } + + /** @type {AlpineAjaxMagic} */ + let ${'$'}ajax; + """.trimIndent() val coreMagics = diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt new file mode 100644 index 0000000..0e2a005 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt @@ -0,0 +1,90 @@ +package com.github.inxilpro.intellijalpine + +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.xml.XmlAttribute +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import com.intellij.util.ProcessingContext + +class AlpineTargetAttributeValueCompletionProvider : CompletionProvider() { + + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val element = parameters.position + val attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute::class.java) ?: return + + if (attribute.name != "x-target") { + return + } + + val xmlFile = element.containingFile as? XmlFile ?: return + val currentValue = attribute.value ?: "" + val allIds = collectElementIds(xmlFile) + val usedIds = currentValue.split("\\s+".toRegex()).filter { it.isNotBlank() }.toSet() + val availableIds = allIds - usedIds + + // Get the prefix for filtering + val caretOffset = parameters.offset + val valueStart = attribute.valueElement?.textRange?.startOffset ?: return + val prefix = currentValue.substring(0, (caretOffset - valueStart - 1).coerceAtLeast(0)) + .split("\\s+".toRegex()).lastOrNull() ?: "" + + val filteredResult = result.withPrefixMatcher(prefix) + + for (id in availableIds) { + if (id.startsWith(prefix)) { + val lookupElement = LookupElementBuilder + .create(id) + .withTypeText("Element ID") + .withIcon(Alpine.ICON) + .withInsertHandler(XTargetInsertHandler()) + + filteredResult.addElement(lookupElement) + } + } + } + + private fun collectElementIds(xmlFile: XmlFile): Set { + val ids = mutableSetOf() + val rootTag = xmlFile.rootTag ?: return ids + + fun collectIds(tag: XmlTag) { + tag.getAttribute("id")?.value?.let { id -> + if (id.isNotBlank()) { + ids.add(id) + } + } + + for (subTag in tag.subTags) { + collectIds(subTag) + } + } + + collectIds(rootTag) + return ids + } +} + +class XTargetInsertHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val element = context.file.findElementAt(context.startOffset) ?: return + val attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute::class.java) ?: return + val currentValue = attribute.value ?: "" + + // If there are already IDs and we're not at the end, add a space + if (currentValue.isNotBlank() && !currentValue.endsWith(" ")) { + context.document.insertString(context.tailOffset, " ") + context.editor.caretModel.moveToOffset(context.tailOffset + 1) + } + } +} \ 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/AttributeInfo.kt index 018c8a8..52c8346 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt @@ -35,6 +35,20 @@ class AttributeInfo(val attribute: String) { "x-wizard:step" to "Add wizard step", "x-wizard:if" to "Add wizard condition", "x-wizard:title" to "Add title to wizard step", + // Alpine AJAX directives + "x-target" to "Enable AJAX for forms/links", + "x-headers" to "Add custom request headers", + "x-merge" to "Control HTML merge strategy", + "x-autofocus" to "Restore keyboard focus", + "x-sync" to "Update non-targeted elements", + // Alpine AJAX merge strategies + "x-merge:before" to "Insert content before target", + "x-merge:replace" to "Replace target element", + "x-merge:update" to "Update target's innerHTML", + "x-merge:prepend" to "Prepend content to target", + "x-merge:append" to "Append content to target", + "x-merge:after" to "Insert content after target", + "x-merge:morph" to "Morph content preserving state", ) val name: String @@ -51,7 +65,7 @@ class AttributeInfo(val attribute: String) { @Suppress("ComplexCondition") fun isAlpine(): Boolean { - return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isWizard() + return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isWizard() || this.isMerge() } fun isEvent(): Boolean { @@ -73,13 +87,17 @@ class AttributeInfo(val attribute: String) { fun isWizard(): Boolean { return "x-wizard:" == prefix } + + fun isMerge(): Boolean { + return "x-merge:" == prefix + } fun hasValue(): Boolean { - return "x-cloak" != name && "x-ignore" != name + return "x-cloak" != name && "x-ignore" != name && "x-sync" != name } fun canBePrefix(): Boolean { - return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-wizard" == name + return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-wizard" == name || "x-merge" == name } @Suppress("ReturnCount") @@ -103,6 +121,10 @@ class AttributeInfo(val attribute: String) { if (attribute.startsWith("x-wizard:")) { return "x-wizard:" } + + if (attribute.startsWith("x-merge:")) { + return "x-merge:" + } return "" } @@ -120,6 +142,10 @@ class AttributeInfo(val attribute: String) { if (isTransition()) { return "CSS classes for '$name' transition phase" } + + if (isMerge()) { + return "HTML merge strategy: '$name'" + } return typeTexts.getOrDefault(attribute, "Alpine.js") } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt index f8cea37..bb185cd 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt @@ -19,6 +19,7 @@ object AttributeUtil { "x-bind", "x-transition", "x-wizard", // glhd/alpine-wizard pacakge + "x-merge", ) val directives = arrayOf( @@ -44,6 +45,12 @@ object AttributeUtil { "x-trap", "x-collapse", "x-spread", // deprecated + // Alpine AJAX directives + "x-target", + "x-headers", + "x-merge", + "x-autofocus", + "x-sync", ) val templateDirectives = arrayOf( @@ -122,6 +129,19 @@ object AttributeUtil { val intersectModifiers = arrayOf( "once" ) + + val targetModifiers = arrayOf( + "422", + "4xx", + "back", + "away", + "replace", + "push" + ) + + val autofocusModifiers = arrayOf( + "nofocus" + ) // Taken from https://developer.mozilla.org/en-US/docs val nameToInterfaceEventMap: Map = mapOf( @@ -276,7 +296,10 @@ object AttributeUtil { } private fun shouldInjectJavaScript(name: String): Boolean { - return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name + // x-target:dynamic should still inject JavaScript, but plain x-target should not + if (name == "x-target") return false + + return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name && "x-autofocus" != name && "x-sync" != name } private fun buildValidAttributes(htmlTag: HtmlTag): Array { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt index 7b23436..59d7994 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt @@ -36,6 +36,16 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String if ("x-intersect" == directive) { addModifiers(directive, AttributeUtil.intersectModifiers) } + + if ("x-target" == directive) { + addModifiers(directive, AttributeUtil.targetModifiers) + // Add x-target:dynamic as a special case + descriptors.add(AttributeInfo("x-target:dynamic")) + } + + if ("x-autofocus" == directive) { + addModifiers(directive, AttributeUtil.autofocusModifiers) + } } } @@ -152,6 +162,14 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String descriptors.add(AttributeInfo("$withExistingModifiers.$origin")) } } + + // Add merge strategy values for x-merge + if (partialAttribute.startsWith("x-merge")) { + val mergeStrategies = arrayOf("before", "replace", "update", "prepend", "append", "after", "morph") + for (strategy in mergeStrategies) { + descriptors.add(AttributeInfo("x-merge:$strategy")) + } + } } private fun getDefaultHtmlAttributes(htmlTag: XmlTag): Array { From 022d8f91b806f946c40c0c980dfd543e2f1c7c47 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 18 Jun 2025 20:39:10 -0400 Subject: [PATCH 02/32] wip --- CLAUDE.md | 6 +- .../AlpineCompletionContributor.kt | 8 -- ...eTargetAttributeValueCompletionProvider.kt | 90 -------------- .../AlpineTargetReferenceContributor.kt | 117 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 2 + 5 files changed, 123 insertions(+), 100 deletions(-) delete mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt diff --git a/CLAUDE.md b/CLAUDE.md index 47e6e04..f721556 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,11 +34,13 @@ This is an IntelliJ IDEA plugin that adds Alpine.js support. The plugin provides 1. **AttributesProvider** - Central class that provides Alpine attribute descriptors to the IDE's HTML/XML support system. It defines which Alpine attributes are available and their properties. -2. **AlpineJavaScriptAttributeValueInjector** - Injects JavaScript language into Alpine attribute values, enabling proper syntax highlighting and code completion within attributes like `x-data` and `x-show`. +2. **AlpineJavaScriptAttributeValueInjector** - Injects JavaScript language into Alpine attribute values, enabling proper syntax highlighting and code completion within attributes like `x-data` and `x-show`. Includes comprehensive type definitions for `$ajax` magic property. 3. **AlpineCompletionContributor** - Handles auto-completion logic for Alpine directives, providing context-aware suggestions based on cursor position. -4. **AttributeInfo** - Contains all Alpine directive definitions and metadata, including documentation and allowed contexts for each directive. +4. **AlpineTargetReferenceContributor** - Provides native IntelliJ reference support for `x-target` attributes, enabling go-to-definition, find usages, refactoring, and error highlighting for ID references. + +5. **AttributeInfo** - Contains all Alpine directive definitions and metadata, including documentation and allowed contexts for each directive. Now includes Alpine AJAX directives (`x-target`, `x-headers`, `x-merge`, `x-autofocus`, `x-sync`). ### Plugin Configuration diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt index ef6516e..1c4ad61 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt @@ -15,13 +15,5 @@ class AlpineCompletionContributor : CompletionContributor() { psiElement(XmlTokenType.XML_NAME).withParent(xmlAttribute()), AlpineAttributeCompletionProvider() ) - - // Attribute value completion for x-target - extend( - CompletionType.BASIC, - psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) - .inside(xmlAttributeValue().withParent(xmlAttribute().withName("x-target"))), - AlpineTargetAttributeValueCompletionProvider() - ) } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt deleted file mode 100644 index 0e2a005..0000000 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetAttributeValueCompletionProvider.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.github.inxilpro.intellijalpine - -import com.intellij.codeInsight.completion.CompletionParameters -import com.intellij.codeInsight.completion.CompletionProvider -import com.intellij.codeInsight.completion.CompletionResultSet -import com.intellij.codeInsight.completion.InsertHandler -import com.intellij.codeInsight.completion.InsertionContext -import com.intellij.codeInsight.lookup.LookupElement -import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.psi.xml.XmlAttribute -import com.intellij.psi.xml.XmlFile -import com.intellij.psi.xml.XmlTag -import com.intellij.util.ProcessingContext - -class AlpineTargetAttributeValueCompletionProvider : CompletionProvider() { - - override fun addCompletions( - parameters: CompletionParameters, - context: ProcessingContext, - result: CompletionResultSet - ) { - val element = parameters.position - val attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute::class.java) ?: return - - if (attribute.name != "x-target") { - return - } - - val xmlFile = element.containingFile as? XmlFile ?: return - val currentValue = attribute.value ?: "" - val allIds = collectElementIds(xmlFile) - val usedIds = currentValue.split("\\s+".toRegex()).filter { it.isNotBlank() }.toSet() - val availableIds = allIds - usedIds - - // Get the prefix for filtering - val caretOffset = parameters.offset - val valueStart = attribute.valueElement?.textRange?.startOffset ?: return - val prefix = currentValue.substring(0, (caretOffset - valueStart - 1).coerceAtLeast(0)) - .split("\\s+".toRegex()).lastOrNull() ?: "" - - val filteredResult = result.withPrefixMatcher(prefix) - - for (id in availableIds) { - if (id.startsWith(prefix)) { - val lookupElement = LookupElementBuilder - .create(id) - .withTypeText("Element ID") - .withIcon(Alpine.ICON) - .withInsertHandler(XTargetInsertHandler()) - - filteredResult.addElement(lookupElement) - } - } - } - - private fun collectElementIds(xmlFile: XmlFile): Set { - val ids = mutableSetOf() - val rootTag = xmlFile.rootTag ?: return ids - - fun collectIds(tag: XmlTag) { - tag.getAttribute("id")?.value?.let { id -> - if (id.isNotBlank()) { - ids.add(id) - } - } - - for (subTag in tag.subTags) { - collectIds(subTag) - } - } - - collectIds(rootTag) - return ids - } -} - -class XTargetInsertHandler : InsertHandler { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val element = context.file.findElementAt(context.startOffset) ?: return - val attribute = PsiTreeUtil.getParentOfType(element, XmlAttribute::class.java) ?: return - val currentValue = attribute.value ?: "" - - // If there are already IDs and we're not at the end, add a space - if (currentValue.isNotBlank() && !currentValue.endsWith(" ")) { - context.document.insertString(context.tailOffset, " ") - context.editor.caretModel.moveToOffset(context.tailOffset + 1) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt new file mode 100644 index 0000000..2a1a791 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt @@ -0,0 +1,117 @@ +package com.github.inxilpro.intellijalpine + +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.TextRange +import com.intellij.patterns.XmlPatterns +import com.intellij.psi.* +import com.intellij.psi.xml.XmlAttribute +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, + private val 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 rootTag = xmlFile.rootTag ?: return null + return findIdInTag(rootTag, id) + } + + private fun findIdInTag(tag: XmlTag, id: String): PsiElement? { + val idAttribute = tag.getAttribute("id") + if (idAttribute?.value == id) { + // Return the value element for better navigation + return idAttribute.valueElement ?: idAttribute + } + + for (subTag in tag.subTags) { + findIdInTag(subTag, id)?.let { return it } + } + + return null + } + + private fun collectElementIds(xmlFile: XmlFile): Set { + val ids = mutableSetOf() + val rootTag = xmlFile.rootTag ?: return ids + + fun collectIds(tag: XmlTag) { + tag.getAttribute("id")?.value?.let { id -> + if (id.isNotBlank()) { + ids.add(id) + } + } + + for (subTag in tag.subTags) { + collectIds(subTag) + } + } + + collectIds(rootTag) + return ids + } +} \ 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..311d4c6 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -24,6 +24,8 @@ + From 959742165a07a259173b74688832e01bd9ff7b85 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 18 Jun 2025 22:02:05 -0400 Subject: [PATCH 03/32] Update AlpineTargetReferenceContributor.kt --- .../AlpineTargetReferenceContributor.kt | 45 +++++-------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt index 2a1a791..806c645 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineTargetReferenceContributor.kt @@ -4,7 +4,7 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.openapi.util.TextRange import com.intellij.patterns.XmlPatterns import com.intellij.psi.* -import com.intellij.psi.xml.XmlAttribute +import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.xml.XmlAttributeValue import com.intellij.psi.xml.XmlFile import com.intellij.psi.xml.XmlTag @@ -51,7 +51,7 @@ class AlpineTargetReferenceProvider : PsiReferenceProvider() { class AlpineIdReference( element: PsiElement, - private val rangeInElement: TextRange, + rangeInElement: TextRange, private val idValue: String ) : PsiReferenceBase(element, rangeInElement) { @@ -77,41 +77,18 @@ class AlpineIdReference( override fun isSoft(): Boolean = false // Hard reference - should show error if unresolved private fun findElementWithId(xmlFile: XmlFile, id: String): PsiElement? { - val rootTag = xmlFile.rootTag ?: return null - return findIdInTag(rootTag, id) - } - - private fun findIdInTag(tag: XmlTag, id: String): PsiElement? { - val idAttribute = tag.getAttribute("id") - if (idAttribute?.value == id) { - // Return the value element for better navigation - return idAttribute.valueElement ?: idAttribute - } + val allTags = PsiTreeUtil.findChildrenOfType(xmlFile, XmlTag::class.java) - for (subTag in tag.subTags) { - findIdInTag(subTag, id)?.let { return it } - } - - return null + return allTags.firstOrNull { tag -> + tag.getAttribute("id")?.value == id + }?.getAttribute("id")?.valueElement } private fun collectElementIds(xmlFile: XmlFile): Set { - val ids = mutableSetOf() - val rootTag = xmlFile.rootTag ?: return ids - - fun collectIds(tag: XmlTag) { - tag.getAttribute("id")?.value?.let { id -> - if (id.isNotBlank()) { - ids.add(id) - } - } - - for (subTag in tag.subTags) { - collectIds(subTag) - } - } - - collectIds(rootTag) - return ids + 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 From 0d399130f8b69d5a5ae6cbf249c95fa8efc6a843 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 18 Jun 2025 22:05:44 -0400 Subject: [PATCH 04/32] Update CLAUDE.md --- CLAUDE.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f721556..3cbdc3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,14 +18,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `./gradlew runInspections` - Run Qodana code inspections - `./gradlew koverReport` - Generate code coverage reports -### Publishing -- `./gradlew patchChangelog` - Update changelog -- `./gradlew signPlugin` - Sign the plugin ZIP -- `./gradlew publishPlugin` - Publish to JetBrains Marketplace (requires token) - ## 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 @@ -33,23 +29,21 @@ This is an IntelliJ IDEA plugin that adds Alpine.js support. The plugin provides ### Key Components 1. **AttributesProvider** - Central class that provides Alpine attribute descriptors to the IDE's HTML/XML support system. It defines which Alpine attributes are available and their properties. - -2. **AlpineJavaScriptAttributeValueInjector** - Injects JavaScript language into Alpine attribute values, enabling proper syntax highlighting and code completion within attributes like `x-data` and `x-show`. Includes comprehensive type definitions for `$ajax` magic property. - +2. **AlpineJavaScriptAttributeValueInjector** - Injects JavaScript language into Alpine attribute values, enabling proper syntax highlighting and code completion within attributes like `x-data` and `x-show`. 3. **AlpineCompletionContributor** - Handles auto-completion logic for Alpine directives, providing context-aware suggestions based on cursor position. - 4. **AlpineTargetReferenceContributor** - Provides native IntelliJ reference support for `x-target` attributes, enabling go-to-definition, find usages, refactoring, and error highlighting for ID references. - -5. **AttributeInfo** - Contains all Alpine directive definitions and metadata, including documentation and allowed contexts for each directive. Now includes Alpine AJAX directives (`x-target`, `x-headers`, `x-merge`, `x-autofocus`, `x-sync`). +5. **AttributeInfo** - Contains all Alpine directive definitions and metadata, including documentation and allowed contexts for each directive. ### 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 2023.1 or newer - JavaScript and HtmlTools plugins as dependencies - Java 17 runtime From be2233bea649183b62a5ad77a12d41856b249672 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 18 Jun 2025 22:31:34 -0400 Subject: [PATCH 05/32] Some clean up --- .../inxilpro/intellijalpine/AttributeUtil.kt | 25 ++++++++++++++----- .../intellijalpine/AutoCompleteSuggestions.kt | 17 ++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt index bb185cd..c11ad01 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt @@ -19,7 +19,7 @@ object AttributeUtil { "x-bind", "x-transition", "x-wizard", // glhd/alpine-wizard pacakge - "x-merge", + "x-target", // alpine-ajax ) val directives = arrayOf( @@ -45,6 +45,7 @@ object AttributeUtil { "x-trap", "x-collapse", "x-spread", // deprecated + // Alpine AJAX directives "x-target", "x-headers", @@ -131,16 +132,28 @@ object AttributeUtil { ) val targetModifiers = arrayOf( + "200", + "301", + "302", + "303", + "400", + "401", + "403", + "404", "422", + "500", + "502", + "503", + "2xx", + "3xx", "4xx", + "5xx", "back", "away", "replace", - "push" - ) - - val autofocusModifiers = arrayOf( - "nofocus" + "push", + "error", + "nofocus", ) // Taken from https://developer.mozilla.org/en-US/docs diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt index 59d7994..c63d7ba 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt @@ -19,6 +19,7 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String addDerivedAttributes() addTransitions() addWizard() + addAjax() } private fun addDirectives() { @@ -36,16 +37,6 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String if ("x-intersect" == directive) { addModifiers(directive, AttributeUtil.intersectModifiers) } - - if ("x-target" == directive) { - addModifiers(directive, AttributeUtil.targetModifiers) - // Add x-target:dynamic as a special case - descriptors.add(AttributeInfo("x-target:dynamic")) - } - - if ("x-autofocus" == directive) { - addModifiers(directive, AttributeUtil.autofocusModifiers) - } } } @@ -95,6 +86,12 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String descriptors.add(AttributeInfo("x-wizard:title")) } + private fun addAjax() { + descriptors.add(AttributeInfo("x-target:dynamic")) + addModifiers("x-target", AttributeUtil.targetModifiers) + addModifiers("x-target:dynamic", AttributeUtil.targetModifiers) + } + private fun addEvent(descriptor: XmlAttributeDescriptor) { val event = descriptor.name.substring(2) for (prefix in AttributeUtil.eventPrefixes) { From e9a4a93e7b0a7577314f8ebdcf674e8cd6593856 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 18 Jun 2025 22:39:53 -0400 Subject: [PATCH 06/32] Fix x-merge --- .../AlpineCompletionContributor.kt | 8 ++++ .../AlpineMergeValueCompletionProvider.kt | 44 +++++++++++++++++++ .../inxilpro/intellijalpine/AttributeInfo.kt | 30 +++++-------- .../inxilpro/intellijalpine/AttributeUtil.kt | 5 +-- .../intellijalpine/AutoCompleteSuggestions.kt | 10 +---- 5 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt index 1c4ad61..5cf3c5a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt @@ -15,5 +15,13 @@ class AlpineCompletionContributor : CompletionContributor() { psiElement(XmlTokenType.XML_NAME).withParent(xmlAttribute()), AlpineAttributeCompletionProvider() ) + + // Attribute value completion for x-merge + extend( + CompletionType.BASIC, + psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) + .inside(xmlAttributeValue().withParent(xmlAttribute().withName("x-merge"))), + AlpineMergeValueCompletionProvider() + ) } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt new file mode 100644 index 0000000..f9fd3cd --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt @@ -0,0 +1,44 @@ +package com.github.inxilpro.intellijalpine + +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +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 : CompletionProvider() { + + 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 addCompletions( + 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/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt index 52c8346..a8837e3 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt @@ -41,14 +41,6 @@ class AttributeInfo(val attribute: String) { "x-merge" to "Control HTML merge strategy", "x-autofocus" to "Restore keyboard focus", "x-sync" to "Update non-targeted elements", - // Alpine AJAX merge strategies - "x-merge:before" to "Insert content before target", - "x-merge:replace" to "Replace target element", - "x-merge:update" to "Update target's innerHTML", - "x-merge:prepend" to "Prepend content to target", - "x-merge:append" to "Append content to target", - "x-merge:after" to "Insert content after target", - "x-merge:morph" to "Morph content preserving state", ) val name: String @@ -65,7 +57,7 @@ class AttributeInfo(val attribute: String) { @Suppress("ComplexCondition") fun isAlpine(): Boolean { - return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isWizard() || this.isMerge() + return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isWizard() || this.isTarget() } fun isEvent(): Boolean { @@ -87,9 +79,9 @@ class AttributeInfo(val attribute: String) { fun isWizard(): Boolean { return "x-wizard:" == prefix } - - fun isMerge(): Boolean { - return "x-merge:" == prefix + + fun isTarget(): Boolean { + return "x-target:" == prefix } fun hasValue(): Boolean { @@ -97,7 +89,7 @@ class AttributeInfo(val attribute: String) { } fun canBePrefix(): Boolean { - return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-wizard" == name || "x-merge" == name + return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-wizard" == name || "x-target" == name } @Suppress("ReturnCount") @@ -121,9 +113,9 @@ class AttributeInfo(val attribute: String) { if (attribute.startsWith("x-wizard:")) { return "x-wizard:" } - - if (attribute.startsWith("x-merge:")) { - return "x-merge:" + + if (attribute.startsWith("x-target:")) { + return "x-target:" } return "" @@ -142,9 +134,9 @@ class AttributeInfo(val attribute: String) { if (isTransition()) { return "CSS classes for '$name' transition phase" } - - if (isMerge()) { - return "HTML merge strategy: '$name'" + + if (isTarget()) { + return "Alpine AJAX target" } return typeTexts.getOrDefault(attribute, "Alpine.js") diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt index c11ad01..bd802ca 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt @@ -19,7 +19,6 @@ object AttributeUtil { "x-bind", "x-transition", "x-wizard", // glhd/alpine-wizard pacakge - "x-target", // alpine-ajax ) val directives = arrayOf( @@ -309,10 +308,10 @@ object AttributeUtil { } private fun shouldInjectJavaScript(name: String): Boolean { - // x-target:dynamic should still inject JavaScript, but plain x-target should not + // `x-target:dynamic` should still inject JavaScript, but plain x-target should not if (name == "x-target") return false - return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name && "x-autofocus" != name && "x-sync" != name + return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name && "x-autofocus" != name && "x-sync" != name && "x-merge" != name } private fun buildValidAttributes(htmlTag: HtmlTag): Array { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt index c63d7ba..7456060 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt @@ -99,7 +99,7 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String addModifiers("$prefix$event", AttributeUtil.eventModifiers) - if (event.toLowerCase() == "keydown" || event.toLowerCase() == "keyup") { + if (event.lowercase() == "keydown" || event.lowercase() == "keyup") { addModifiers("$prefix$event", AttributeUtil.keypressModifiers) } } @@ -159,14 +159,6 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String descriptors.add(AttributeInfo("$withExistingModifiers.$origin")) } } - - // Add merge strategy values for x-merge - if (partialAttribute.startsWith("x-merge")) { - val mergeStrategies = arrayOf("before", "replace", "update", "prepend", "append", "after", "morph") - for (strategy in mergeStrategies) { - descriptors.add(AttributeInfo("x-merge:$strategy")) - } - } } private fun getDefaultHtmlAttributes(htmlTag: XmlTag): Array { From f9555b64c9ec46a575fbe5062308946ec4dee505 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 13:42:51 -0400 Subject: [PATCH 07/32] Fix $ajax --- .../AlpineJavaScriptAttributeValueInjector.kt | 63 ++++--------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt index c276472..394baa6 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt @@ -89,57 +89,6 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { */ function ${'$'}queryString(value) {} - class AlpineAjaxMagic { - /** - * @param {string} url - * @param {Object} options - * @return {Promise} - */ - get(url, options = {}) {} - - /** - * @param {string} url - * @param {Object} data - * @param {Object} options - * @return {Promise} - */ - post(url, data = {}, options = {}) {} - - /** - * @param {string} url - * @param {Object} data - * @param {Object} options - * @return {Promise} - */ - put(url, data = {}, options = {}) {} - - /** - * @param {string} url - * @param {Object} data - * @param {Object} options - * @return {Promise} - */ - patch(url, data = {}, options = {}) {} - - /** - * @param {string} url - * @param {Object} options - * @return {Promise} - */ - delete(url, options = {}) {} - - /** - * @param {string} url - * @param {FormData} formData - * @param {Object} options - * @return {Promise} - */ - submit(url, formData, options = {}) {} - } - - /** @type {AlpineAjaxMagic} */ - let ${'$'}ajax; - """.trimIndent() val coreMagics = @@ -179,6 +128,16 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { """.trimIndent() val eventMagics = "/** @type {eventType} */\nlet ${'$'}event;\n\n" + + val ajaxMagics = """ + /** + * @param {string} action + * @param {Object} options + * @return string[] + */ + function ${'$'}ajax(action, options = {}) {} + + """.trimIndent() } override fun getLanguagesToInject(registrar: MultiHostRegistrar, host: PsiElement) { @@ -243,7 +202,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { } private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { - val context = MutablePair(globalMagics, "") + val context = MutablePair(globalMagics + ajaxMagics, "") if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left From 0a180dcebb237091feeb8504ae68eb5d0a6bbba2 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 15:33:34 -0400 Subject: [PATCH 08/32] wip --- .gitignore | 1 + .../intellijalpine/AlpineAjaxDetector.kt | 232 ++++++++++++++++++ .../AlpineJavaScriptAttributeValueInjector.kt | 19 +- .../intellijalpine/AlpineProjectActivity.kt | 10 + .../intellijalpine/AlpineProjectListener.kt | 10 + .../AlpineProjectSettingsState.kt | 26 ++ .../intellijalpine/AlpineSettingsComponent.kt | 26 +- .../AlpineSettingsConfigurable.kt | 36 ++- .../inxilpro/intellijalpine/AttributeUtil.kt | 19 +- src/main/resources/META-INF/plugin.xml | 13 +- 10 files changed, 362 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt 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/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt new file mode 100644 index 0000000..6347831 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt @@ -0,0 +1,232 @@ +package com.github.inxilpro.intellijalpine + +import com.intellij.json.psi.JsonFile +import com.intellij.json.psi.JsonObject +import com.intellij.json.psi.JsonProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import com.intellij.psi.PsiFile +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 +import com.intellij.util.messages.MessageBusConnection +import java.util.concurrent.ConcurrentHashMap + +object AlpineAjaxDetector { + private val listeners = ConcurrentHashMap() + + fun checkAndAutoEnable(project: Project) { + val projectSettings = AlpineProjectSettingsState.getInstance(project) + + // Only auto-enable if currently disabled + if (!projectSettings.enableAlpineAjax) { + if (performDetection(project)) { + projectSettings.enableAlpineAjax = true + } + } + + // Set up listener for package.json changes if not already set up + 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) { + // Re-check and potentially auto-enable on package.json changes + checkAndAutoEnable(project) + } + } + }) + } + + fun cleanup(project: Project) { + val projectPath = project.basePath ?: project.name + listeners.remove(projectPath)?.disconnect() + } + + private fun performDetection(project: Project): Boolean { + return hasAlpineAjaxInPackageJson(project) || hasAlpineAjaxInScriptTags(project) || hasAlpineAjaxCode(project) + } + + private fun hasAlpineAjaxInPackageJson(project: Project): 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 + + hasAlpineAjaxDependency(dependencies) || hasAlpineAjaxDependency(devDependencies) + } else { + false + } + } + } + + private fun hasAlpineAjaxDependency(dependencies: JsonObject?): Boolean { + return dependencies?.findProperty("alpine-ajax") != null || + dependencies?.findProperty("@imacrayon/alpine-ajax") != null + } + + private fun hasAlpineAjaxInScriptTags(project: Project): Boolean { + val htmlFiles = mutableListOf() + + // Get files by extension + val extensions = listOf("html", "htm", "php", "twig") + 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 { + psiFile is XmlFile -> { + // Handle HTML and XML files + val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) + .filter { it.name.equals("script", ignoreCase = true) } + + scriptTags.any { scriptTag -> + val src = scriptTag.getAttributeValue("src") + if (src != null) { + // Check script src attributes + isAlpineAjaxScriptSrc(src) + } else { + // Check inline script content + hasAlpineAjaxPatterns(scriptTag.value.text) + } + } + } + virtualFile.name.endsWith(".blade.php") -> { + // Handle Blade files by checking their raw content + try { + val content = String(virtualFile.contentsToByteArray()) + hasAlpineAjaxPatterns(content) || hasAlpineAjaxScriptTagsInContent(content) + } catch (_: Exception) { + false + } + } + else -> false + } + } + } + + private fun hasAlpineAjaxCode(project: Project): Boolean { + val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") + 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()) + hasAlpineAjaxPatterns(content) + } catch (_: Exception) { + false + } + } + } + + private fun isAlpineAjaxScriptSrc(src: String): Boolean { + return src.contains("alpine-ajax", ignoreCase = true) || + src.contains("imacrayon/alpine-ajax", ignoreCase = true) || + src.contains("unpkg.com/@imacrayon/alpine-ajax", ignoreCase = true) || + src.contains("jsdelivr.net/npm/@imacrayon/alpine-ajax", ignoreCase = true) || + src.contains("unpkg.com/alpine-ajax", ignoreCase = true) || + src.contains("jsdelivr.net/npm/alpine-ajax", ignoreCase = true) + } + + private fun hasAlpineAjaxScriptTagsInContent(content: String): Boolean { + // Use regex to find script tags with alpine-ajax references in raw HTML content + val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) + + return scriptTagRegex.findAll(content).any { match -> + val src = match.groupValues[1] + isAlpineAjaxScriptSrc(src) + } + } + + private fun hasAlpineAjaxPatterns(content: String): Boolean { + val alpineAjaxPatterns = listOf( + // Import statements + "import.*alpine-ajax", + "from.*alpine-ajax", + "require.*alpine-ajax", + + // Alpine.js plugin registration + "Alpine\\.plugin\\s*\\(.*ajax", + "alpine\\.plugin\\s*\\(.*ajax", + + // Ajax-specific functions from the alpine-ajax source + "AjaxInterceptor", + "AjaxCommand", + "ajaxCommand", + "processAjaxResponse", + "handleAjaxRequest", + + // Magic helper usage patterns + "\\\$ajax\\s*\\(", + "x-target", + "x-swap", + "x-headers", + "x-replace" + ) + + return alpineAjaxPatterns.any { pattern -> + Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) + } + } + + fun getAlpineAjaxMagics(): String { + return """ + /** + * @param {string} action + * @param {Object} options + * @return {Promise} + */ + function ${'$'}ajax(action, options = {}) {} + + """.trimIndent() + } +} \ 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/AlpineJavaScriptAttributeValueInjector.kt index 394baa6..4d0b08f 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt @@ -128,16 +128,6 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { """.trimIndent() val eventMagics = "/** @type {eventType} */\nlet ${'$'}event;\n\n" - - val ajaxMagics = """ - /** - * @param {string} action - * @param {Object} options - * @return string[] - */ - function ${'$'}ajax(action, options = {}) {} - - """.trimIndent() } override fun getLanguagesToInject(registrar: MultiHostRegistrar, host: PsiElement) { @@ -202,7 +192,14 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { } private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { - val context = MutablePair(globalMagics + ajaxMagics, "") + var magics = globalMagics + + // Conditionally add alpine-ajax magics if enabled in project settings + if (AlpineProjectSettingsState.getInstance(host.project).enableAlpineAjax) { + magics += AlpineAjaxDetector.getAlpineAjaxMagics() + } + + val context = MutablePair(magics, "") if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt new file mode 100644 index 0000000..c710fa9 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt @@ -0,0 +1,10 @@ +package com.github.inxilpro.intellijalpine + +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity + +class AlpineProjectActivity : ProjectActivity { + override suspend fun execute(project: Project) { + AlpineAjaxDetector.checkAndAutoEnable(project) + } +} diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt new file mode 100644 index 0000000..7c57512 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt @@ -0,0 +1,10 @@ +package com.github.inxilpro.intellijalpine + +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManagerListener + +class AlpineProjectListener : ProjectManagerListener { + override fun projectClosed(project: Project) { + AlpineAjaxDetector.cleanup(project) + } +} diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt new file mode 100644 index 0000000..26efd25 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt @@ -0,0 +1,26 @@ +package com.github.inxilpro.intellijalpine + +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("alpine-project.xml")]) +class AlpineProjectSettingsState : PersistentStateComponent { + var enableAlpineAjax = false + + 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/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt index b54b701..c1bf0c9 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt @@ -1,14 +1,17 @@ package com.github.inxilpro.intellijalpine +import com.intellij.openapi.project.Project +import com.intellij.ui.TitledSeparator import com.intellij.ui.components.JBCheckBox import com.intellij.util.ui.FormBuilder import javax.swing.JComponent import javax.swing.JPanel -class AlpineSettingsComponent { +class AlpineSettingsComponent(private val project: Project?) { val panel: JPanel - private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons? ") + private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons") + private val myEnableAlpineAjaxStatus = JBCheckBox("Enable alpine-ajax support for this project") val preferredFocusedComponent: JComponent get() = myShowGutterIconsStatus @@ -19,10 +22,23 @@ class AlpineSettingsComponent { myShowGutterIconsStatus.isSelected = newStatus } + var enableAlpineAjaxStatus: Boolean + get() = myEnableAlpineAjaxStatus.isSelected + set(newStatus) { + myEnableAlpineAjaxStatus.isSelected = newStatus + } + init { - panel = FormBuilder.createFormBuilder() + val builder = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator("Application Settings")) .addComponent(myShowGutterIconsStatus, 1) - .addComponentFillVertically(JPanel(), 0) - .panel + + // Only show project settings if we have a project context + if (project != null) { + builder.addComponent(TitledSeparator("Project Settings")) + .addComponent(myEnableAlpineAjaxStatus, 1) + } + + panel = builder.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 index 875096f..f1b16c9 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt @@ -1,9 +1,10 @@ package com.github.inxilpro.intellijalpine import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project import javax.swing.JComponent -class AlpineSettingsConfigurable : Configurable { +class AlpineSettingsConfigurable(private val project: Project?) : Configurable { private var mySettingsComponent: AlpineSettingsComponent? = null override fun getDisplayName(): String { @@ -15,24 +16,43 @@ class AlpineSettingsConfigurable : Configurable { } override fun createComponent(): JComponent? { - mySettingsComponent = AlpineSettingsComponent() + mySettingsComponent = AlpineSettingsComponent(project) return mySettingsComponent?.panel } override fun isModified(): Boolean { - val settings: AlpineSettingsState = AlpineSettingsState.instance + val appSettings = AlpineSettingsState.instance + var isModified = mySettingsComponent?.showGutterIconsStatus != appSettings.showGutterIcons - return mySettingsComponent?.showGutterIconsStatus != settings.showGutterIcons + // Check project settings if we have a project + if (project != null) { + val projectSettings = AlpineProjectSettingsState.getInstance(project) + isModified = isModified || mySettingsComponent?.enableAlpineAjaxStatus != projectSettings.enableAlpineAjax + } + + return isModified } override fun apply() { - val settings: AlpineSettingsState = AlpineSettingsState.instance - settings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false + val appSettings = AlpineSettingsState.instance + appSettings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false + + // Apply project settings if we have a project + if (project != null) { + val projectSettings = AlpineProjectSettingsState.getInstance(project) + projectSettings.enableAlpineAjax = mySettingsComponent?.enableAlpineAjaxStatus != false + } } override fun reset() { - val settings: AlpineSettingsState = AlpineSettingsState.instance - mySettingsComponent?.showGutterIconsStatus = settings.showGutterIcons + val appSettings = AlpineSettingsState.instance + mySettingsComponent?.showGutterIconsStatus = appSettings.showGutterIcons + + // Reset project settings if we have a project + if (project != null) { + val projectSettings = AlpineProjectSettingsState.getInstance(project) + mySettingsComponent?.enableAlpineAjaxStatus = projectSettings.enableAlpineAjax + } } override fun disposeUIResources() { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt index fb0f5f4..16df736 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt @@ -21,7 +21,7 @@ object AttributeUtil { "x-wizard", // glhd/alpine-wizard pacakge ) - val directives = arrayOf( + private val coreDirectives = arrayOf( "x-data", "x-init", "x-show", @@ -44,8 +44,9 @@ object AttributeUtil { "x-trap", "x-collapse", "x-spread", // deprecated + ) - // Alpine AJAX directives + private val ajaxDirectives = arrayOf( "x-target", "x-headers", "x-merge", @@ -53,6 +54,17 @@ object AttributeUtil { "x-sync", ) + val directives: Array + get() = coreDirectives + ajaxDirectives + + fun getDirectivesForProject(project: com.intellij.openapi.project.Project, contextFile: com.intellij.psi.PsiFile?): Array { + return if (AlpineProjectSettingsState.getInstance(project).enableAlpineAjax) { + coreDirectives + ajaxDirectives + } else { + coreDirectives + } + } + val templateDirectives = arrayOf( "x-if", "x-for", @@ -320,8 +332,9 @@ object AttributeUtil { private fun buildValidAttributes(htmlTag: HtmlTag): Array { val descriptors = mutableListOf() + val projectDirectives = getDirectivesForProject(htmlTag.project, htmlTag.containingFile) - for (directive in directives) { + for (directive in projectDirectives) { if (htmlTag.name != "template" && isTemplateDirective(directive)) { continue } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 311d4c6..17608f7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -29,8 +29,15 @@ - + + + + + + + From b8fbf6afe231052e815ab7ffb0923cfaba5d9329 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 16:23:32 -0400 Subject: [PATCH 09/32] wip --- .../intellijalpine/AlpineAjaxPlugin.kt | 224 ++++++++++++++++++ .../AlpineJavaScriptAttributeValueInjector.kt | 11 +- .../inxilpro/intellijalpine/AlpinePlugin.kt | 29 +++ .../intellijalpine/AlpinePluginRegistry.kt | 93 ++++++++ .../intellijalpine/AlpineProjectActivity.kt | 5 +- .../intellijalpine/AlpineProjectListener.kt | 2 +- .../inxilpro/intellijalpine/AttributeInfo.kt | 16 +- .../inxilpro/intellijalpine/AttributeUtil.kt | 7 +- src/main/resources/META-INF/plugin.xml | 9 + 9 files changed, 370 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt new file mode 100644 index 0000000..9273646 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt @@ -0,0 +1,224 @@ +package com.github.inxilpro.intellijalpine + +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 +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import org.apache.commons.lang3.tuple.MutablePair + +class AlpineAjaxPlugin : AlpinePlugin { + + 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 getDirectives(): List = listOf( + "x-target", + "x-headers", + "x-merge", + "x-autofocus", + "x-sync" + ) + + override fun isEnabled(project: Project): Boolean { + return AlpineProjectSettingsState.getInstance(project).enableAlpineAjax + } + + override fun enable(project: Project) { + AlpineProjectSettingsState.getInstance(project).enableAlpineAjax = true + } + + override fun disable(project: Project) { + AlpineProjectSettingsState.getInstance(project).enableAlpineAjax = false + } + + override fun performDetection(project: Project): Boolean { + return hasAlpineAjaxInPackageJson(project) || + hasAlpineAjaxInScriptTags(project) || + hasAlpineAjaxCode(project) + } + + private fun hasAlpineAjaxInPackageJson(project: Project): 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 + + hasAlpineAjaxDependency(dependencies) || hasAlpineAjaxDependency(devDependencies) + } else { + false + } + } + } + + private fun hasAlpineAjaxDependency(dependencies: JsonObject?): Boolean { + return getPackageNamesForDetection().any { packageName -> + dependencies?.findProperty(packageName) != null + } + } + + private fun hasAlpineAjaxInScriptTags(project: Project): Boolean { + val htmlFiles = mutableListOf() + + // Get files by extension + val extensions = listOf("html", "htm", "php", "twig") + 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 { + psiFile is XmlFile -> { + // Handle HTML and XML files + val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) + .filter { it.name.equals("script", ignoreCase = true) } + + scriptTags.any { scriptTag -> + val src = scriptTag.getAttributeValue("src") + if (src != null) { + isAlpineAjaxScriptSrc(src) + } else { + hasAlpineAjaxPatterns(scriptTag.value.text) + } + } + } + virtualFile.name.endsWith(".blade.php") -> { + // Handle Blade files by checking their raw content + try { + val content = String(virtualFile.contentsToByteArray()) + hasAlpineAjaxPatterns(content) || hasAlpineAjaxScriptTagsInContent(content) + } catch (_: Exception) { + false + } + } + else -> false + } + } + } + + private fun isAlpineAjaxScriptSrc(src: String): Boolean { + return src.contains("alpine-ajax", ignoreCase = true) || + src.contains("imacrayon/alpine-ajax", ignoreCase = true) || + src.contains("unpkg.com/@imacrayon/alpine-ajax", ignoreCase = true) || + src.contains("jsdelivr.net/npm/@imacrayon/alpine-ajax", ignoreCase = true) || + src.contains("unpkg.com/alpine-ajax", ignoreCase = true) || + src.contains("jsdelivr.net/npm/alpine-ajax", ignoreCase = true) + } + + private fun hasAlpineAjaxScriptTagsInContent(content: String): Boolean { + // Use regex to find script tags with alpine-ajax references in raw HTML content + val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) + + return scriptTagRegex.findAll(content).any { match -> + val src = match.groupValues[1] + isAlpineAjaxScriptSrc(src) + } + } + + private fun hasAlpineAjaxCode(project: Project): Boolean { + val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") + 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()) + hasAlpineAjaxPatterns(content) + } catch (_: Exception) { + false + } + } + } + + private fun hasAlpineAjaxPatterns(content: String): Boolean { + val alpineAjaxPatterns = listOf( + // Import statements + "import.*alpine-ajax", + "from.*alpine-ajax", + "require.*alpine-ajax", + + // Alpine.js plugin registration + "Alpine\\.plugin\\s*\\(.*ajax", + "alpine\\.plugin\\s*\\(.*ajax", + + // Ajax-specific functions from the alpine-ajax source + "AjaxInterceptor", + "AjaxCommand", + "ajaxCommand", + "processAjaxResponse", + "handleAjaxRequest", + + // Magic helper usage patterns + "\\\$ajax\\s*\\(", + "x-target", + "x-swap", + "x-headers", + "x-replace" + ) + + return alpineAjaxPatterns.any { pattern -> + Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) + } + } +} \ 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/AlpineJavaScriptAttributeValueInjector.kt index 4d0b08f..640066c 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt @@ -12,7 +12,6 @@ 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.* @@ -192,14 +191,8 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { } private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { - var magics = globalMagics - - // Conditionally add alpine-ajax magics if enabled in project settings - if (AlpineProjectSettingsState.getInstance(host.project).enableAlpineAjax) { - magics += AlpineAjaxDetector.getAlpineAjaxMagics() - } - - val context = MutablePair(magics, "") + val globalContext = MutablePair(globalMagics, ""); + val context = AlpinePluginRegistry.getInstance().injectAllJsContext(host.project, globalContext) if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt new file mode 100644 index 0000000..0fedfea --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt @@ -0,0 +1,29 @@ +package com.github.inxilpro.intellijalpine + +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.create("com.github.inxilpro.intellijalpine.alpinePlugin") + } + + fun getPackageDisplayName(): String + + fun getPackageNamesForDetection(): List + + fun getTypeText(info: AttributeInfo): String? + + fun injectJsContext(context: MutablePair): MutablePair + + fun getDirectives(): List + + fun isEnabled(project: Project): Boolean + + fun enable(project: Project) + + fun disable(project: Project) + + fun performDetection(project: Project): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt new file mode 100644 index 0000000..c4bf593 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt @@ -0,0 +1,93 @@ +package com.github.inxilpro.intellijalpine + +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() + + companion object { + fun getInstance(): AlpinePluginRegistry { + return com.intellij.openapi.application.ApplicationManager.getApplication() + .getService(AlpinePluginRegistry::class.java) + } + } + + fun getRegisteredPlugins(): List { + return AlpinePlugin.EP_NAME.extensionList + } + + fun getEnabledPlugins(project: Project): List { + return getRegisteredPlugins().filter { it.isEnabled(project) } + } + + fun getAllDirectives(project: Project): List { + return getEnabledPlugins(project).flatMap { it.getDirectives() } + } + + 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 checkAndAutoEnablePlugins(project: Project) { + getRegisteredPlugins().forEach { plugin -> + if (!plugin.isEnabled(project) && plugin.performDetection(project)) { + plugin.enable(project) + } + } + + // Set up package.json listener for auto-enabling plugins + 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) { + // Re-check and potentially auto-enable plugins on package.json changes + ApplicationManager.getApplication().runReadAction { + getRegisteredPlugins().forEach { plugin -> + if (!plugin.isEnabled(project) && plugin.performDetection(project)) { + plugin.enable(project) + } + } + } + } + } + }) + } + + 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/AlpineProjectActivity.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt index c710fa9..87e818e 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt @@ -1,10 +1,13 @@ package com.github.inxilpro.intellijalpine +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) { - AlpineAjaxDetector.checkAndAutoEnable(project) + readAction { + AlpinePluginRegistry.getInstance().checkAndAutoEnablePlugins(project) + } } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt index 7c57512..1bf81bf 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt @@ -5,6 +5,6 @@ import com.intellij.openapi.project.ProjectManagerListener class AlpineProjectListener : ProjectManagerListener { override fun projectClosed(project: Project) { - AlpineAjaxDetector.cleanup(project) + AlpinePluginRegistry.getInstance().cleanup(project) } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt index a8837e3..adc669a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt @@ -35,12 +35,6 @@ class AttributeInfo(val attribute: String) { "x-wizard:step" to "Add wizard step", "x-wizard:if" to "Add wizard condition", "x-wizard:title" to "Add title to wizard step", - // Alpine AJAX directives - "x-target" to "Enable AJAX for forms/links", - "x-headers" to "Add custom request headers", - "x-merge" to "Control HTML merge strategy", - "x-autofocus" to "Restore keyboard focus", - "x-sync" to "Update non-targeted elements", ) val name: String @@ -81,7 +75,7 @@ class AttributeInfo(val attribute: String) { } fun isTarget(): Boolean { - return "x-target:" == prefix + return "x-target:" == prefix // TODO: Move to plugin system (and in canBePrefix) } fun hasValue(): Boolean { @@ -135,10 +129,12 @@ class AttributeInfo(val attribute: String) { return "CSS classes for '$name' transition phase" } - if (isTarget()) { - return "Alpine AJAX target" + // First check plugin registry for type text + val pluginTypeText = AlpinePluginRegistry.getInstance().getTypeText(this) + if (pluginTypeText != null) { + return pluginTypeText } - + return typeTexts.getOrDefault(attribute, "Alpine.js") } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt index 16df736..e299362 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt @@ -58,11 +58,8 @@ object AttributeUtil { get() = coreDirectives + ajaxDirectives fun getDirectivesForProject(project: com.intellij.openapi.project.Project, contextFile: com.intellij.psi.PsiFile?): Array { - return if (AlpineProjectSettingsState.getInstance(project).enableAlpineAjax) { - coreDirectives + ajaxDirectives - } else { - coreDirectives - } + val pluginDirectives = AlpinePluginRegistry.getInstance().getAllDirectives(project) + return (coreDirectives.toList() + pluginDirectives).toTypedArray() } val templateDirectives = arrayOf( diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 17608f7..4a40b7c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -15,6 +15,10 @@ com.intellij.modules.xml JavaScript + + + + @@ -29,6 +33,7 @@ + + + + + From 4528c838ea380a7fe161df28a38a80e5b0817f8d Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 16:24:49 -0400 Subject: [PATCH 10/32] wip --- .../intellijalpine/AlpineAjaxDetector.kt | 232 ------------------ .../intellijalpine/AlpineAjaxPlugin.kt | 7 +- 2 files changed, 1 insertion(+), 238 deletions(-) delete mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt deleted file mode 100644 index 6347831..0000000 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxDetector.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.github.inxilpro.intellijalpine - -import com.intellij.json.psi.JsonFile -import com.intellij.json.psi.JsonObject -import com.intellij.json.psi.JsonProperty -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.newvfs.BulkFileListener -import com.intellij.openapi.vfs.newvfs.events.VFileEvent -import com.intellij.psi.PsiFile -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 -import com.intellij.util.messages.MessageBusConnection -import java.util.concurrent.ConcurrentHashMap - -object AlpineAjaxDetector { - private val listeners = ConcurrentHashMap() - - fun checkAndAutoEnable(project: Project) { - val projectSettings = AlpineProjectSettingsState.getInstance(project) - - // Only auto-enable if currently disabled - if (!projectSettings.enableAlpineAjax) { - if (performDetection(project)) { - projectSettings.enableAlpineAjax = true - } - } - - // Set up listener for package.json changes if not already set up - 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) { - // Re-check and potentially auto-enable on package.json changes - checkAndAutoEnable(project) - } - } - }) - } - - fun cleanup(project: Project) { - val projectPath = project.basePath ?: project.name - listeners.remove(projectPath)?.disconnect() - } - - private fun performDetection(project: Project): Boolean { - return hasAlpineAjaxInPackageJson(project) || hasAlpineAjaxInScriptTags(project) || hasAlpineAjaxCode(project) - } - - private fun hasAlpineAjaxInPackageJson(project: Project): 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 - - hasAlpineAjaxDependency(dependencies) || hasAlpineAjaxDependency(devDependencies) - } else { - false - } - } - } - - private fun hasAlpineAjaxDependency(dependencies: JsonObject?): Boolean { - return dependencies?.findProperty("alpine-ajax") != null || - dependencies?.findProperty("@imacrayon/alpine-ajax") != null - } - - private fun hasAlpineAjaxInScriptTags(project: Project): Boolean { - val htmlFiles = mutableListOf() - - // Get files by extension - val extensions = listOf("html", "htm", "php", "twig") - 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 { - psiFile is XmlFile -> { - // Handle HTML and XML files - val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) - .filter { it.name.equals("script", ignoreCase = true) } - - scriptTags.any { scriptTag -> - val src = scriptTag.getAttributeValue("src") - if (src != null) { - // Check script src attributes - isAlpineAjaxScriptSrc(src) - } else { - // Check inline script content - hasAlpineAjaxPatterns(scriptTag.value.text) - } - } - } - virtualFile.name.endsWith(".blade.php") -> { - // Handle Blade files by checking their raw content - try { - val content = String(virtualFile.contentsToByteArray()) - hasAlpineAjaxPatterns(content) || hasAlpineAjaxScriptTagsInContent(content) - } catch (_: Exception) { - false - } - } - else -> false - } - } - } - - private fun hasAlpineAjaxCode(project: Project): Boolean { - val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") - 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()) - hasAlpineAjaxPatterns(content) - } catch (_: Exception) { - false - } - } - } - - private fun isAlpineAjaxScriptSrc(src: String): Boolean { - return src.contains("alpine-ajax", ignoreCase = true) || - src.contains("imacrayon/alpine-ajax", ignoreCase = true) || - src.contains("unpkg.com/@imacrayon/alpine-ajax", ignoreCase = true) || - src.contains("jsdelivr.net/npm/@imacrayon/alpine-ajax", ignoreCase = true) || - src.contains("unpkg.com/alpine-ajax", ignoreCase = true) || - src.contains("jsdelivr.net/npm/alpine-ajax", ignoreCase = true) - } - - private fun hasAlpineAjaxScriptTagsInContent(content: String): Boolean { - // Use regex to find script tags with alpine-ajax references in raw HTML content - val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) - - return scriptTagRegex.findAll(content).any { match -> - val src = match.groupValues[1] - isAlpineAjaxScriptSrc(src) - } - } - - private fun hasAlpineAjaxPatterns(content: String): Boolean { - val alpineAjaxPatterns = listOf( - // Import statements - "import.*alpine-ajax", - "from.*alpine-ajax", - "require.*alpine-ajax", - - // Alpine.js plugin registration - "Alpine\\.plugin\\s*\\(.*ajax", - "alpine\\.plugin\\s*\\(.*ajax", - - // Ajax-specific functions from the alpine-ajax source - "AjaxInterceptor", - "AjaxCommand", - "ajaxCommand", - "processAjaxResponse", - "handleAjaxRequest", - - // Magic helper usage patterns - "\\\$ajax\\s*\\(", - "x-target", - "x-swap", - "x-headers", - "x-replace" - ) - - return alpineAjaxPatterns.any { pattern -> - Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) - } - } - - fun getAlpineAjaxMagics(): String { - return """ - /** - * @param {string} action - * @param {Object} options - * @return {Promise} - */ - function ${'$'}ajax(action, options = {}) {} - - """.trimIndent() - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt index 9273646..041e675 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt @@ -149,12 +149,7 @@ class AlpineAjaxPlugin : AlpinePlugin { } private fun isAlpineAjaxScriptSrc(src: String): Boolean { - return src.contains("alpine-ajax", ignoreCase = true) || - src.contains("imacrayon/alpine-ajax", ignoreCase = true) || - src.contains("unpkg.com/@imacrayon/alpine-ajax", ignoreCase = true) || - src.contains("jsdelivr.net/npm/@imacrayon/alpine-ajax", ignoreCase = true) || - src.contains("unpkg.com/alpine-ajax", ignoreCase = true) || - src.contains("jsdelivr.net/npm/alpine-ajax", ignoreCase = true) + return src.contains("alpine-ajax", ignoreCase = true) } private fun hasAlpineAjaxScriptTagsInContent(content: String): Boolean { From 5f0d10ef9ef925b7ddc6a001fab005a234c488e9 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 17:23:47 -0400 Subject: [PATCH 11/32] Move wizard logic to plugin --- .../intellijalpine/AlpineAjaxPlugin.kt | 4 + .../AlpineJavaScriptAttributeValueInjector.kt | 45 +--- .../inxilpro/intellijalpine/AlpinePlugin.kt | 2 + .../intellijalpine/AlpinePluginRegistry.kt | 4 + .../AlpineProjectSettingsState.kt | 1 + .../intellijalpine/AlpineSettingsComponent.kt | 8 + .../AlpineSettingsConfigurable.kt | 6 +- .../intellijalpine/AlpineWizardPlugin.kt | 249 ++++++++++++++++++ .../inxilpro/intellijalpine/AttributeInfo.kt | 13 +- .../inxilpro/intellijalpine/AttributeUtil.kt | 11 +- .../intellijalpine/AutoCompleteSuggestions.kt | 2 +- src/main/resources/META-INF/plugin.xml | 1 + 12 files changed, 287 insertions(+), 59 deletions(-) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineWizardPlugin.kt diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt index 041e675..3d2c8ab 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt @@ -57,6 +57,10 @@ class AlpineAjaxPlugin : AlpinePlugin { "x-sync" ) + override fun getPrefixes(): List = listOf( + "x-target" + ) + override fun isEnabled(project: Project): Boolean { return AlpineProjectSettingsState.getInstance(project).enableAlpineAjax } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt index 640066c..0df795a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt @@ -28,49 +28,6 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { """.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 = """ @@ -243,7 +200,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}" } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt index 0fedfea..c2cd142 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt @@ -19,6 +19,8 @@ interface AlpinePlugin { fun getDirectives(): List + fun getPrefixes(): List + fun isEnabled(project: Project): Boolean fun enable(project: Project) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt index c4bf593..7e2b0a1 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt @@ -33,6 +33,10 @@ class AlpinePluginRegistry { 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) } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt index 26efd25..b08d05e 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt @@ -9,6 +9,7 @@ import com.intellij.util.xmlb.XmlSerializerUtil @State(name = "com.github.inxilpro.intellijalpine.AlpineProjectSettingsState", storages = [Storage("alpine-project.xml")]) class AlpineProjectSettingsState : PersistentStateComponent { var enableAlpineAjax = false + var enableAlpineWizard = false override fun getState(): AlpineProjectSettingsState? { return this diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt index c1bf0c9..b4ac2a2 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt @@ -12,6 +12,7 @@ class AlpineSettingsComponent(private val project: Project?) { private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons") private val myEnableAlpineAjaxStatus = JBCheckBox("Enable alpine-ajax support for this project") + private val myEnableAlpineWizardStatus = JBCheckBox("Enable alpine-wizard support for this project") val preferredFocusedComponent: JComponent get() = myShowGutterIconsStatus @@ -28,6 +29,12 @@ class AlpineSettingsComponent(private val project: Project?) { myEnableAlpineAjaxStatus.isSelected = newStatus } + var enableAlpineWizardStatus: Boolean + get() = myEnableAlpineWizardStatus.isSelected + set(newStatus) { + myEnableAlpineWizardStatus.isSelected = newStatus + } + init { val builder = FormBuilder.createFormBuilder() .addComponent(TitledSeparator("Application Settings")) @@ -37,6 +44,7 @@ class AlpineSettingsComponent(private val project: Project?) { if (project != null) { builder.addComponent(TitledSeparator("Project Settings")) .addComponent(myEnableAlpineAjaxStatus, 1) + .addComponent(myEnableAlpineWizardStatus, 1) } panel = builder.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 index f1b16c9..38926e8 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt @@ -27,7 +27,9 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { // Check project settings if we have a project if (project != null) { val projectSettings = AlpineProjectSettingsState.getInstance(project) - isModified = isModified || mySettingsComponent?.enableAlpineAjaxStatus != projectSettings.enableAlpineAjax + isModified = isModified || + mySettingsComponent?.enableAlpineAjaxStatus != projectSettings.enableAlpineAjax || + mySettingsComponent?.enableAlpineWizardStatus != projectSettings.enableAlpineWizard } return isModified @@ -41,6 +43,7 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { if (project != null) { val projectSettings = AlpineProjectSettingsState.getInstance(project) projectSettings.enableAlpineAjax = mySettingsComponent?.enableAlpineAjaxStatus != false + projectSettings.enableAlpineWizard = mySettingsComponent?.enableAlpineWizardStatus != false } } @@ -52,6 +55,7 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { if (project != null) { val projectSettings = AlpineProjectSettingsState.getInstance(project) mySettingsComponent?.enableAlpineAjaxStatus = projectSettings.enableAlpineAjax + mySettingsComponent?.enableAlpineWizardStatus = projectSettings.enableAlpineWizard } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineWizardPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineWizardPlugin.kt new file mode 100644 index 0000000..ad0dfa3 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineWizardPlugin.kt @@ -0,0 +1,249 @@ +package com.github.inxilpro.intellijalpine + +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 +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import org.apache.commons.lang3.tuple.MutablePair + +class AlpineWizardPlugin : AlpinePlugin { + + 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 getDirectives(): List = listOf( + "x-wizard:step", + "x-wizard:if", + "x-wizard:title" + ) + + override fun getPrefixes(): List = listOf( + "x-wizard" + ) + + override fun isEnabled(project: Project): Boolean { + return AlpineProjectSettingsState.getInstance(project).enableAlpineWizard + } + + override fun enable(project: Project) { + AlpineProjectSettingsState.getInstance(project).enableAlpineWizard = true + } + + override fun disable(project: Project) { + AlpineProjectSettingsState.getInstance(project).enableAlpineWizard = false + } + + override fun performDetection(project: Project): Boolean { + return hasAlpineWizardInPackageJson(project) || + hasAlpineWizardInScriptTags(project) || + hasAlpineWizardCode(project) + } + + private fun hasAlpineWizardInPackageJson(project: Project): 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 + + hasAlpineWizardDependency(dependencies) || hasAlpineWizardDependency(devDependencies) + } else { + false + } + } + } + + private fun hasAlpineWizardDependency(dependencies: JsonObject?): Boolean { + return getPackageNamesForDetection().any { packageName -> + dependencies?.findProperty(packageName) != null + } + } + + private fun hasAlpineWizardInScriptTags(project: Project): Boolean { + val htmlFiles = mutableListOf() + + // Get files by extension + val extensions = listOf("html", "htm", "php", "twig") + 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 { + psiFile is XmlFile -> { + // Handle HTML and XML files + val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) + .filter { it.name.equals("script", ignoreCase = true) } + + scriptTags.any { scriptTag -> + val src = scriptTag.getAttributeValue("src") + if (src != null) { + isAlpineWizardScriptSrc(src) + } else { + hasAlpineWizardPatterns(scriptTag.value.text) + } + } + } + virtualFile.name.endsWith(".blade.php") -> { + // Handle Blade files by checking their raw content + try { + val content = String(virtualFile.contentsToByteArray()) + hasAlpineWizardPatterns(content) || hasAlpineWizardScriptTagsInContent(content) + } catch (_: Exception) { + false + } + } + else -> false + } + } + } + + private fun isAlpineWizardScriptSrc(src: String): Boolean { + return src.contains("alpine-wizard", ignoreCase = true) + } + + private fun hasAlpineWizardScriptTagsInContent(content: String): Boolean { + // Use regex to find script tags with alpine-wizard references in raw HTML content + val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) + + return scriptTagRegex.findAll(content).any { match -> + val src = match.groupValues[1] + isAlpineWizardScriptSrc(src) + } + } + + private fun hasAlpineWizardCode(project: Project): Boolean { + val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") + 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()) + hasAlpineWizardPatterns(content) + } catch (_: Exception) { + false + } + } + } + + private fun hasAlpineWizardPatterns(content: String): Boolean { + val alpineWizardPatterns = listOf( + // Import statements + "import.*alpine-wizard", + "from.*alpine-wizard", + "require.*alpine-wizard", + + // Alpine.js plugin registration + "Alpine\\.plugin\\s*\\(.*wizard", + "alpine\\.plugin\\s*\\(.*wizard", + + // Wizard-specific usage patterns + "\\\$wizard\\s*\\.", + "x-wizard:", + "Alpine\\.wizard", + "alpine\\.wizard" + ) + + return alpineWizardPatterns.any { pattern -> + Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) + } + } +} \ 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/AttributeInfo.kt index adc669a..6300196 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt @@ -32,9 +32,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 @@ -51,7 +48,7 @@ class AttributeInfo(val attribute: String) { @Suppress("ComplexCondition") fun isAlpine(): Boolean { - return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isWizard() || this.isTarget() + return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isTarget() } fun isEvent(): Boolean { @@ -70,9 +67,6 @@ class AttributeInfo(val attribute: String) { return AttributeUtil.directives.contains(name) } - fun isWizard(): Boolean { - return "x-wizard:" == prefix - } fun isTarget(): Boolean { return "x-target:" == prefix // TODO: Move to plugin system (and in canBePrefix) @@ -83,7 +77,7 @@ class AttributeInfo(val attribute: String) { } fun canBePrefix(): Boolean { - return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-wizard" == name || "x-target" == name + return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-target" == name } @Suppress("ReturnCount") @@ -104,9 +98,6 @@ class AttributeInfo(val attribute: String) { return "x-transition:" } - if (attribute.startsWith("x-wizard:")) { - return "x-wizard:" - } if (attribute.startsWith("x-target:")) { return "x-target:" diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt index e299362..ab050d7 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt @@ -14,12 +14,14 @@ import java.util.Collections object AttributeUtil { private val validAttributes = mutableMapOf>() - val xmlPrefixes = arrayOf( + private val coreXmlPrefixes = arrayOf( "x-on", "x-bind", "x-transition", - "x-wizard", // glhd/alpine-wizard pacakge ) + + val xmlPrefixes: Array + get() = coreXmlPrefixes // TODO: Merge in plugin prefixes private val coreDirectives = arrayOf( "x-data", @@ -61,6 +63,11 @@ object AttributeUtil { val pluginDirectives = AlpinePluginRegistry.getInstance().getAllDirectives(project) return (coreDirectives.toList() + pluginDirectives).toTypedArray() } + + fun getXmlPrefixesForProject(project: com.intellij.openapi.project.Project): Array { + val pluginPrefixes = AlpinePluginRegistry.getInstance().getAllPrefixes(project) + return (coreXmlPrefixes.toList() + pluginPrefixes).toTypedArray() + } val templateDirectives = arrayOf( "x-if", diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt index 7456060..96f42fe 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt @@ -41,7 +41,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)) } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 4a40b7c..c30f64c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -43,6 +43,7 @@ + From 1dc1c8189d23f85871b086203530a248cc61e4e0 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 17:57:35 -0400 Subject: [PATCH 12/32] Restructuring --- .../AlpineAttributeDescriptor.kt | 6 +- .../{ => attributes}/AttributeInfo.kt | 10 +- .../{ => attributes}/AttributeUtil.kt | 52 +++----- .../{ => attributes}/AttributesProvider.kt | 4 +- .../AlpineAttributeCompletionProvider.kt | 8 +- .../AlpineCompletionContributor.kt | 18 +-- .../AutoCompleteSuggestions.kt | 29 ++--- .../{ => completion}/AutoPopupHandler.kt | 4 +- .../{ => core}/AlpineLineMarkerProvider.kt | 9 +- .../intellijalpine/{ => core}/AlpinePlugin.kt | 8 +- .../{ => core}/AlpinePluginRegistry.kt | 40 +++--- .../AlpineJavaScriptAttributeValueInjector.kt | 14 +- .../{ => plugins}/AlpineAjaxPlugin.kt | 122 ++++++++++++------ .../AlpineMergeValueCompletionProvider.kt | 15 ++- .../AlpineTargetReferenceContributor.kt | 3 +- .../{ => plugins}/AlpineWizardPlugin.kt | 95 ++++++++------ .../{ => settings}/AlpineProjectActivity.kt | 7 +- .../{ => settings}/AlpineProjectListener.kt | 7 +- .../AlpineProjectSettingsState.kt | 2 +- .../{ => settings}/AlpineSettingsComponent.kt | 4 +- .../AlpineSettingsConfigurable.kt | 14 +- .../{ => settings}/AlpineSettingsState.kt | 4 +- .../{ => support}/LanguageUtil.kt | 8 +- .../{ => support}/XmlExtension.kt | 9 +- src/main/resources/META-INF/plugin.xml | 34 ++--- 25 files changed, 286 insertions(+), 240 deletions(-) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => attributes}/AlpineAttributeDescriptor.kt (87%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => attributes}/AttributeInfo.kt (94%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => attributes}/AttributeUtil.kt (91%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => attributes}/AttributesProvider.kt (93%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => completion}/AlpineAttributeCompletionProvider.kt (89%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => completion}/AlpineCompletionContributor.kt (50%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => completion}/AutoCompleteSuggestions.kt (85%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => completion}/AutoPopupHandler.kt (94%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => core}/AlpineLineMarkerProvider.kt (77%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => core}/AlpinePlugin.kt (63%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => core}/AlpinePluginRegistry.kt (88%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => injection}/AlpineJavaScriptAttributeValueInjector.kt (95%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => plugins}/AlpineAjaxPlugin.kt (80%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => plugins}/AlpineMergeValueCompletionProvider.kt (90%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => plugins}/AlpineTargetReferenceContributor.kt (97%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => plugins}/AlpineWizardPlugin.kt (86%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => settings}/AlpineProjectActivity.kt (57%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => settings}/AlpineProjectListener.kt (53%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => settings}/AlpineProjectSettingsState.kt (94%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => settings}/AlpineSettingsComponent.kt (97%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => settings}/AlpineSettingsConfigurable.kt (84%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => settings}/AlpineSettingsState.kt (94%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => support}/LanguageUtil.kt (87%) rename src/main/kotlin/com/github/inxilpro/intellijalpine/{ => support}/XmlExtension.kt (83%) 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 87% 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..3382cb0 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,7 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.attributes +import com.github.inxilpro.intellijalpine.Alpine +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo import com.intellij.psi.PsiElement import com.intellij.psi.meta.PsiPresentableMetaData import com.intellij.psi.xml.XmlTag @@ -46,4 +48,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 94% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 6300196..19ff7a7 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -1,4 +1,6 @@ -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) { @@ -121,11 +123,11 @@ class AttributeInfo(val attribute: String) { } // First check plugin registry for type text - val pluginTypeText = AlpinePluginRegistry.getInstance().getTypeText(this) + val pluginTypeText = AlpinePluginRegistry.Companion.getInstance().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 91% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index ab050d7..59aa710 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -1,5 +1,10 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.attributes +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.support.LanguageUtil +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile import com.intellij.psi.html.HtmlTag import com.intellij.psi.impl.source.html.dtd.HtmlAttributeDescriptorImpl import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl @@ -8,8 +13,6 @@ 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>() @@ -19,7 +22,7 @@ object AttributeUtil { "x-bind", "x-transition", ) - + val xmlPrefixes: Array get() = coreXmlPrefixes // TODO: Merge in plugin prefixes @@ -59,13 +62,13 @@ object AttributeUtil { val directives: Array get() = coreDirectives + ajaxDirectives - fun getDirectivesForProject(project: com.intellij.openapi.project.Project, contextFile: com.intellij.psi.PsiFile?): Array { - val pluginDirectives = AlpinePluginRegistry.getInstance().getAllDirectives(project) + fun getDirectivesForProject(project: Project, contextFile: PsiFile?): Array { + val pluginDirectives = AlpinePluginRegistry.Companion.getInstance().getAllDirectives(project) return (coreDirectives.toList() + pluginDirectives).toTypedArray() } - - fun getXmlPrefixesForProject(project: com.intellij.openapi.project.Project): Array { - val pluginPrefixes = AlpinePluginRegistry.getInstance().getAllPrefixes(project) + + fun getXmlPrefixesForProject(project: Project): Array { + val pluginPrefixes = AlpinePluginRegistry.Companion.getInstance().getAllPrefixes(project) return (coreXmlPrefixes.toList() + pluginPrefixes).toTypedArray() } @@ -145,31 +148,6 @@ object AttributeUtil { val intersectModifiers = arrayOf( "once" ) - - 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", - ) // Taken from https://developer.mozilla.org/en-US/docs val nameToInterfaceEventMap: Map = mapOf( @@ -280,7 +258,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 @@ -330,7 +308,7 @@ object AttributeUtil { private fun shouldInjectJavaScript(name: String): Boolean { // `x-target:dynamic` should still inject JavaScript, but plain x-target should not if (name == "x-target") return false - + return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name && "x-autofocus" != name && "x-sync" != name && "x-merge" != name } @@ -368,4 +346,4 @@ object AttributeUtil { return (descriptor as? HtmlElementDescriptorImpl)?.getDefaultAttributeDescriptors(xmlTag) ?: emptyArray() } -} +} \ 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 89% 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..5ae2a39 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,14 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.completion +import com.github.inxilpro.intellijalpine.Alpine +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +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 +61,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/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt similarity index 50% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt index 5cf3c5a..61df83f 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineCompletionContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt @@ -1,10 +1,10 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.completion +import com.github.inxilpro.intellijalpine.plugins.AlpineMergeValueCompletionProvider 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.patterns.XmlPatterns.xmlAttributeValue +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.XmlPatterns import com.intellij.psi.xml.XmlTokenType class AlpineCompletionContributor : CompletionContributor() { @@ -12,16 +12,16 @@ class AlpineCompletionContributor : CompletionContributor() { // Attribute name completion extend( CompletionType.BASIC, - psiElement(XmlTokenType.XML_NAME).withParent(xmlAttribute()), + PlatformPatterns.psiElement(XmlTokenType.XML_NAME).withParent(XmlPatterns.xmlAttribute()), AlpineAttributeCompletionProvider() ) - + // Attribute value completion for x-merge extend( CompletionType.BASIC, - psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) - .inside(xmlAttributeValue().withParent(xmlAttribute().withName("x-merge"))), + PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) + .inside(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("x-merge"))), AlpineMergeValueCompletionProvider() ) } -} +} \ 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 85% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AutoCompleteSuggestions.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt index 96f42fe..b1a48aa 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,13 +20,12 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String addPrefixes() addDerivedAttributes() addTransitions() - addWizard() - addAjax() + addPlugins() } private fun addDirectives() { for (directive in AttributeUtil.directives) { - if (tagName != "template" && isTemplateDirective(directive)) { + if (tagName != "template" && AttributeUtil.isTemplateDirective(directive)) { continue } @@ -78,18 +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 addAjax() { - descriptors.add(AttributeInfo("x-target:dynamic")) - addModifiers("x-target", AttributeUtil.targetModifiers) - addModifiers("x-target:dynamic", AttributeUtil.targetModifiers) + private fun addPlugins() { + AlpinePluginRegistry.getInstance().injectAllAutoCompleteSuggestions(htmlTag.project, this) } private fun addEvent(descriptor: XmlAttributeDescriptor) { @@ -111,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 } @@ -167,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 77% 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..81362cb 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 @@ -18,7 +21,7 @@ class AlpineLineMarkerProvider : RelatedItemLineMarkerProvider() { element: PsiElement, result: MutableCollection?> ) { - if (!AlpineSettingsState.instance.showGutterIcons) return + if (!AlpineSettingsState.Companion.instance.showGutterIcons) return if (element is XmlAttribute && element.descriptor is AlpineAttributeDescriptor) { @@ -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/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt similarity index 63% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt index c2cd142..0a1e8b3 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -1,12 +1,14 @@ -package com.github.inxilpro.intellijalpine +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.create("com.github.inxilpro.intellijalpine.alpinePlugin") + val EP_NAME = ExtensionPointName.Companion.create("com.github.inxilpro.intellijalpine.alpinePlugin") } fun getPackageDisplayName(): String @@ -17,6 +19,8 @@ interface AlpinePlugin { fun injectJsContext(context: MutablePair): MutablePair + fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) + fun getDirectives(): List fun getPrefixes(): List diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt similarity index 88% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt index 7e2b0a1..9836341 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -1,5 +1,7 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.core +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project @@ -13,69 +15,73 @@ import java.util.concurrent.ConcurrentHashMap @Service(Service.Level.APP) class AlpinePluginRegistry { private val listeners = ConcurrentHashMap() - + companion object { fun getInstance(): AlpinePluginRegistry { - return com.intellij.openapi.application.ApplicationManager.getApplication() + return ApplicationManager.getApplication() .getService(AlpinePluginRegistry::class.java) } } - + fun getRegisteredPlugins(): List { return AlpinePlugin.EP_NAME.extensionList } - + fun getEnabledPlugins(project: Project): List { return getRegisteredPlugins().filter { it.isEnabled(project) } } - + 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 (!plugin.isEnabled(project) && plugin.performDetection(project)) { plugin.enable(project) } } - + // Set up package.json listener for auto-enabling plugins 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) { // Re-check and potentially auto-enable plugins on package.json changes ApplicationManager.getApplication().runReadAction { @@ -89,7 +95,7 @@ class AlpinePluginRegistry { } }) } - + fun cleanup(project: Project) { val projectPath = project.basePath ?: project.name listeners.remove(projectPath)?.disconnect() 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 95% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineJavaScriptAttributeValueInjector.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt index 0df795a..524a770 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.core.AlpinePluginRegistry +import com.github.inxilpro.intellijalpine.support.LanguageUtil +import com.github.inxilpro.intellijalpine.attributes.AttributeUtil +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 @@ -14,7 +17,6 @@ import com.intellij.psi.xml.XmlAttributeValue import com.intellij.psi.xml.XmlTag import org.apache.commons.lang3.tuple.MutablePair import org.apache.html.dom.HTMLDocumentImpl -import java.util.* class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { private companion object { @@ -102,7 +104,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) @@ -149,7 +151,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { val globalContext = MutablePair(globalMagics, ""); - val context = AlpinePluginRegistry.getInstance().injectAllJsContext(host.project, globalContext) + val context = AlpinePluginRegistry.Companion.getInstance().injectAllJsContext(host.project, globalContext) if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left @@ -245,4 +247,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/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt similarity index 80% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index 3d2c8ab..0d76723 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -1,8 +1,14 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.plugins +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.github.inxilpro.intellijalpine.settings.AlpineProjectSettingsState +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.attributes.AttributeUtil +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonObject 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 @@ -12,14 +18,39 @@ import com.intellij.psi.xml.XmlTag 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 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" @@ -34,7 +65,7 @@ class AlpineAjaxPlugin : AlpinePlugin { else -> null } } - + override fun injectJsContext(context: MutablePair): MutablePair { val magics = """ /** @@ -45,69 +76,76 @@ class AlpineAjaxPlugin : AlpinePlugin { function ${'$'}ajax(action, options = {}) {} """.trimIndent() - + return MutablePair(context.left + magics, context.right) } - + + + 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 getDirectives(): List = listOf( "x-target", - "x-headers", + "x-headers", "x-merge", "x-autofocus", "x-sync" ) - + override fun getPrefixes(): List = listOf( "x-target" ) - + override fun isEnabled(project: Project): Boolean { - return AlpineProjectSettingsState.getInstance(project).enableAlpineAjax + return AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineAjax } - + override fun enable(project: Project) { - AlpineProjectSettingsState.getInstance(project).enableAlpineAjax = true + AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineAjax = true } - + override fun disable(project: Project) { - AlpineProjectSettingsState.getInstance(project).enableAlpineAjax = false + AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineAjax = false } - + override fun performDetection(project: Project): Boolean { - return hasAlpineAjaxInPackageJson(project) || - hasAlpineAjaxInScriptTags(project) || + return hasAlpineAjaxInPackageJson(project) || + hasAlpineAjaxInScriptTags(project) || hasAlpineAjaxCode(project) } - + private fun hasAlpineAjaxInPackageJson(project: Project): Boolean { val packageJsonFiles = FilenameIndex.getVirtualFilesByName( - "package.json", + "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 - + hasAlpineAjaxDependency(dependencies) || hasAlpineAjaxDependency(devDependencies) } else { false } } } - + private fun hasAlpineAjaxDependency(dependencies: JsonObject?): Boolean { return getPackageNamesForDetection().any { packageName -> dependencies?.findProperty(packageName) != null } } - + private fun hasAlpineAjaxInScriptTags(project: Project): Boolean { - val htmlFiles = mutableListOf() - + val htmlFiles = mutableListOf() + // Get files by extension val extensions = listOf("html", "htm", "php", "twig") for (extension in extensions) { @@ -119,16 +157,16 @@ class AlpineAjaxPlugin : AlpinePlugin { ) ) } - + return htmlFiles.any { virtualFile -> val psiFile = PsiManager.getInstance(project).findFile(virtualFile) - + when { psiFile is XmlFile -> { // Handle HTML and XML files val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) .filter { it.name.equals("script", ignoreCase = true) } - + scriptTags.any { scriptTag -> val src = scriptTag.getAttributeValue("src") if (src != null) { @@ -151,25 +189,25 @@ class AlpineAjaxPlugin : AlpinePlugin { } } } - + private fun isAlpineAjaxScriptSrc(src: String): Boolean { return src.contains("alpine-ajax", ignoreCase = true) } - + private fun hasAlpineAjaxScriptTagsInContent(content: String): Boolean { // Use regex to find script tags with alpine-ajax references in raw HTML content val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) - + return scriptTagRegex.findAll(content).any { match -> val src = match.groupValues[1] isAlpineAjaxScriptSrc(src) } } - + private fun hasAlpineAjaxCode(project: Project): Boolean { val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") - val jsFiles = mutableListOf() - + val jsFiles = mutableListOf() + for (extension in jsExtensions) { jsFiles.addAll( FilenameIndex.getAllFilesByExt( @@ -179,7 +217,7 @@ class AlpineAjaxPlugin : AlpinePlugin { ) ) } - + return jsFiles.any { virtualFile -> try { val content = String(virtualFile.contentsToByteArray()) @@ -189,25 +227,25 @@ class AlpineAjaxPlugin : AlpinePlugin { } } } - + private fun hasAlpineAjaxPatterns(content: String): Boolean { val alpineAjaxPatterns = listOf( // Import statements "import.*alpine-ajax", "from.*alpine-ajax", "require.*alpine-ajax", - + // Alpine.js plugin registration "Alpine\\.plugin\\s*\\(.*ajax", "alpine\\.plugin\\s*\\(.*ajax", - + // Ajax-specific functions from the alpine-ajax source "AjaxInterceptor", - "AjaxCommand", + "AjaxCommand", "ajaxCommand", "processAjaxResponse", "handleAjaxRequest", - + // Magic helper usage patterns "\\\$ajax\\s*\\(", "x-target", @@ -215,7 +253,7 @@ class AlpineAjaxPlugin : AlpinePlugin { "x-headers", "x-replace" ) - + return alpineAjaxPatterns.any { pattern -> Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt similarity index 90% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt index f9fd3cd..12a92c3 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineMergeValueCompletionProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt @@ -1,5 +1,6 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.plugins +import com.github.inxilpro.intellijalpine.Alpine import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider import com.intellij.codeInsight.completion.CompletionResultSet @@ -9,17 +10,17 @@ import com.intellij.psi.xml.XmlAttribute import com.intellij.util.ProcessingContext class AlpineMergeValueCompletionProvider : CompletionProvider() { - + 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", + "append" to "Append content to target", "after" to "Insert content after target", "morph" to "Morph content preserving state" ) - + override fun addCompletions( parameters: CompletionParameters, context: ProcessingContext, @@ -27,17 +28,17 @@ class AlpineMergeValueCompletionProvider : CompletionProvider = listOf( "alpine-wizard", "@glhd/alpine-wizard" ) - + override fun getTypeText(info: AttributeInfo): String? { if ("x-wizard:" == info.prefix) { return when (info.name) { @@ -29,7 +34,7 @@ class AlpineWizardPlugin : AlpinePlugin { else -> "Alpine Wizard directive" } } - + return when (info.attribute) { "x-wizard:step" -> "Define wizard step" "x-wizard:if" -> "Conditional wizard step" @@ -37,7 +42,7 @@ class AlpineWizardPlugin : AlpinePlugin { else -> null } } - + override fun injectJsContext(context: MutablePair): MutablePair { val wizardMagics = """ class AlpineWizardStep { @@ -81,67 +86,75 @@ class AlpineWizardPlugin : AlpinePlugin { 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:if", "x-wizard:title" ) - + override fun getPrefixes(): List = listOf( "x-wizard" ) - + override fun isEnabled(project: Project): Boolean { - return AlpineProjectSettingsState.getInstance(project).enableAlpineWizard + return AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineWizard } - + override fun enable(project: Project) { - AlpineProjectSettingsState.getInstance(project).enableAlpineWizard = true + AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineWizard = true } - + override fun disable(project: Project) { - AlpineProjectSettingsState.getInstance(project).enableAlpineWizard = false + AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineWizard = false } - + override fun performDetection(project: Project): Boolean { - return hasAlpineWizardInPackageJson(project) || - hasAlpineWizardInScriptTags(project) || + return hasAlpineWizardInPackageJson(project) || + hasAlpineWizardInScriptTags(project) || hasAlpineWizardCode(project) } - + private fun hasAlpineWizardInPackageJson(project: Project): Boolean { val packageJsonFiles = FilenameIndex.getVirtualFilesByName( - "package.json", + "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 - + hasAlpineWizardDependency(dependencies) || hasAlpineWizardDependency(devDependencies) } else { false } } } - + private fun hasAlpineWizardDependency(dependencies: JsonObject?): Boolean { return getPackageNamesForDetection().any { packageName -> dependencies?.findProperty(packageName) != null } } - + private fun hasAlpineWizardInScriptTags(project: Project): Boolean { - val htmlFiles = mutableListOf() - + val htmlFiles = mutableListOf() + // Get files by extension val extensions = listOf("html", "htm", "php", "twig") for (extension in extensions) { @@ -153,16 +166,16 @@ class AlpineWizardPlugin : AlpinePlugin { ) ) } - + return htmlFiles.any { virtualFile -> val psiFile = PsiManager.getInstance(project).findFile(virtualFile) - + when { psiFile is XmlFile -> { // Handle HTML and XML files val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) .filter { it.name.equals("script", ignoreCase = true) } - + scriptTags.any { scriptTag -> val src = scriptTag.getAttributeValue("src") if (src != null) { @@ -185,25 +198,25 @@ class AlpineWizardPlugin : AlpinePlugin { } } } - + private fun isAlpineWizardScriptSrc(src: String): Boolean { return src.contains("alpine-wizard", ignoreCase = true) } - + private fun hasAlpineWizardScriptTagsInContent(content: String): Boolean { // Use regex to find script tags with alpine-wizard references in raw HTML content val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) - + return scriptTagRegex.findAll(content).any { match -> val src = match.groupValues[1] isAlpineWizardScriptSrc(src) } } - + private fun hasAlpineWizardCode(project: Project): Boolean { val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") - val jsFiles = mutableListOf() - + val jsFiles = mutableListOf() + for (extension in jsExtensions) { jsFiles.addAll( FilenameIndex.getAllFilesByExt( @@ -213,7 +226,7 @@ class AlpineWizardPlugin : AlpinePlugin { ) ) } - + return jsFiles.any { virtualFile -> try { val content = String(virtualFile.contentsToByteArray()) @@ -223,25 +236,25 @@ class AlpineWizardPlugin : AlpinePlugin { } } } - + private fun hasAlpineWizardPatterns(content: String): Boolean { val alpineWizardPatterns = listOf( // Import statements "import.*alpine-wizard", "from.*alpine-wizard", "require.*alpine-wizard", - + // Alpine.js plugin registration "Alpine\\.plugin\\s*\\(.*wizard", "alpine\\.plugin\\s*\\(.*wizard", - + // Wizard-specific usage patterns "\\\$wizard\\s*\\.", "x-wizard:", "Alpine\\.wizard", "alpine\\.wizard" ) - + return alpineWizardPatterns.any { pattern -> Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt similarity index 57% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt index 87e818e..985adae 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectActivity.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt @@ -1,5 +1,6 @@ -package com.github.inxilpro.intellijalpine +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 @@ -7,7 +8,7 @@ import com.intellij.openapi.startup.ProjectActivity class AlpineProjectActivity : ProjectActivity { override suspend fun execute(project: Project) { readAction { - AlpinePluginRegistry.getInstance().checkAndAutoEnablePlugins(project) + AlpinePluginRegistry.Companion.getInstance().checkAndAutoEnablePlugins(project) } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt similarity index 53% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt index 1bf81bf..dfe8fc2 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectListener.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt @@ -1,10 +1,11 @@ -package com.github.inxilpro.intellijalpine +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.getInstance().cleanup(project) + AlpinePluginRegistry.Companion.getInstance().cleanup(project) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt similarity index 94% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt index b08d05e..778a9c7 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineProjectSettingsState.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt @@ -1,4 +1,4 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.settings import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt similarity index 97% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt index b4ac2a2..b6c6c90 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -1,4 +1,4 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.settings import com.intellij.openapi.project.Project import com.intellij.ui.TitledSeparator @@ -49,4 +49,4 @@ class AlpineSettingsComponent(private val project: Project?) { panel = builder.addComponentFillVertically(JPanel(), 0).panel } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt similarity index 84% rename from src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt rename to src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt index 38926e8..d9806d3 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt @@ -1,5 +1,7 @@ -package com.github.inxilpro.intellijalpine +package com.github.inxilpro.intellijalpine.settings +import com.github.inxilpro.intellijalpine.settings.AlpineSettingsComponent +import com.github.inxilpro.intellijalpine.settings.AlpineSettingsState import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project import javax.swing.JComponent @@ -21,13 +23,13 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { } override fun isModified(): Boolean { - val appSettings = AlpineSettingsState.instance + val appSettings = AlpineSettingsState.Companion.instance var isModified = mySettingsComponent?.showGutterIconsStatus != appSettings.showGutterIcons // Check project settings if we have a project if (project != null) { val projectSettings = AlpineProjectSettingsState.getInstance(project) - isModified = isModified || + isModified = isModified || mySettingsComponent?.enableAlpineAjaxStatus != projectSettings.enableAlpineAjax || mySettingsComponent?.enableAlpineWizardStatus != projectSettings.enableAlpineWizard } @@ -36,7 +38,7 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { } override fun apply() { - val appSettings = AlpineSettingsState.instance + val appSettings = AlpineSettingsState.Companion.instance appSettings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false // Apply project settings if we have a project @@ -48,7 +50,7 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { } override fun reset() { - val appSettings = AlpineSettingsState.instance + val appSettings = AlpineSettingsState.Companion.instance mySettingsComponent?.showGutterIconsStatus = appSettings.showGutterIcons // Reset project settings if we have a project @@ -62,4 +64,4 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { 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 87% 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..596825f 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 @@ -38,11 +36,11 @@ object LanguageUtil { } private fun isHtmlBasedLanguage(language: Language): Boolean { - if (language.isKindOf(HTMLLanguage.INSTANCE)) { + if (language.isKindOf(HTMLLanguage.Companion.INSTANCE)) { return true } - if (language.isKindOf(XMLLanguage.INSTANCE)) { + if (language.isKindOf(XMLLanguage.Companion.INSTANCE)) { return TEMPLATE_LANGUAGE_IDS.contains(language.id) } 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 83% 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..3e435a5 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,7 @@ -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.github.inxilpro.intellijalpine.support.LanguageUtil import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile import com.intellij.psi.html.HtmlTag @@ -9,7 +10,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 +33,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 c30f64c..e656c32 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -16,38 +16,38 @@ JavaScript - + - - - - + + + + + implementation="com.github.inxilpro.intellijalpine.plugins.AlpineTargetReferenceContributor"/> - - - - + + + + - + - - + + - From f850bde7249c2817a15f40fd37b6939f9dcf0cde Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 18:01:31 -0400 Subject: [PATCH 13/32] defaults --- .../inxilpro/intellijalpine/core/AlpinePlugin.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt index 0a1e8b3..fddec16 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -15,15 +15,15 @@ interface AlpinePlugin { fun getPackageNamesForDetection(): List - fun getTypeText(info: AttributeInfo): String? + fun getTypeText(info: AttributeInfo): String? = null - fun injectJsContext(context: MutablePair): MutablePair + fun injectJsContext(context: MutablePair): MutablePair = context - fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) + fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {} - fun getDirectives(): List + fun getDirectives(): List = emptyList() - fun getPrefixes(): List + fun getPrefixes(): List = emptyList() fun isEnabled(project: Project): Boolean @@ -31,5 +31,5 @@ interface AlpinePlugin { fun disable(project: Project) - fun performDetection(project: Project): Boolean + fun performDetection(project: Project): Boolean = false } \ No newline at end of file From db18ea3fc64a794471137588c8d8d4ad7c800081 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 18:12:41 -0400 Subject: [PATCH 14/32] Clean up plugin system --- .../intellijalpine/core/AlpinePlugin.kt | 8 ++--- .../core/AlpinePluginRegistry.kt | 24 ++++++++++--- .../plugins/AlpineAjaxPlugin.kt | 14 ++------ .../plugins/AlpineWizardPlugin.kt | 14 ++------ .../settings/AlpineProjectSettingsState.kt | 11 ++++-- .../settings/AlpineSettingsComponent.kt | 29 +++++++-------- .../settings/AlpineSettingsConfigurable.kt | 36 +++++++++++++------ src/main/resources/META-INF/plugin.xml | 1 - 8 files changed, 75 insertions(+), 62 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt index fddec16..cc48043 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -11,6 +11,8 @@ interface AlpinePlugin { val EP_NAME = ExtensionPointName.Companion.create("com.github.inxilpro.intellijalpine.alpinePlugin") } + fun getPluginName(): String + fun getPackageDisplayName(): String fun getPackageNamesForDetection(): List @@ -25,11 +27,5 @@ interface AlpinePlugin { fun getPrefixes(): List = emptyList() - fun isEnabled(project: Project): Boolean - - fun enable(project: Project) - - fun disable(project: Project) - 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 index 9836341..27ea1e4 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -2,6 +2,7 @@ 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.settings.AlpineProjectSettingsState import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project @@ -28,7 +29,20 @@ class AlpinePluginRegistry { } fun getEnabledPlugins(project: Project): List { - return getRegisteredPlugins().filter { it.isEnabled(project) } + 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 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 { @@ -55,8 +69,8 @@ class AlpinePluginRegistry { fun checkAndAutoEnablePlugins(project: Project) { getRegisteredPlugins().forEach { plugin -> - if (!plugin.isEnabled(project) && plugin.performDetection(project)) { - plugin.enable(project) + if (!isPluginEnabled(project, plugin.getPluginName()) && plugin.performDetection(project)) { + enablePlugin(project, plugin.getPluginName()) } } @@ -86,8 +100,8 @@ class AlpinePluginRegistry { // Re-check and potentially auto-enable plugins on package.json changes ApplicationManager.getApplication().runReadAction { getRegisteredPlugins().forEach { plugin -> - if (!plugin.isEnabled(project) && plugin.performDetection(project)) { - plugin.enable(project) + if (!isPluginEnabled(project, plugin.getPluginName()) && plugin.performDetection(project)) { + enablePlugin(project, plugin.getPluginName()) } } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index 0d76723..d20d47d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -44,6 +44,8 @@ class AlpineAjaxPlugin : AlpinePlugin { "nofocus", ) + override fun getPluginName(): String = "alpine-ajax" + override fun getPackageDisplayName(): String = "alpine-ajax" override fun getPackageNamesForDetection(): List = listOf( @@ -99,18 +101,6 @@ class AlpineAjaxPlugin : AlpinePlugin { "x-target" ) - override fun isEnabled(project: Project): Boolean { - return AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineAjax - } - - override fun enable(project: Project) { - AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineAjax = true - } - - override fun disable(project: Project) { - AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineAjax = false - } - override fun performDetection(project: Project): Boolean { return hasAlpineAjaxInPackageJson(project) || hasAlpineAjaxInScriptTags(project) || diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt index 59d5dc0..cab9d27 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt @@ -18,6 +18,8 @@ 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( @@ -108,18 +110,6 @@ class AlpineWizardPlugin : AlpinePlugin { "x-wizard" ) - override fun isEnabled(project: Project): Boolean { - return AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineWizard - } - - override fun enable(project: Project) { - AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineWizard = true - } - - override fun disable(project: Project) { - AlpineProjectSettingsState.Companion.getInstance(project).enableAlpineWizard = false - } - override fun performDetection(project: Project): Boolean { return hasAlpineWizardInPackageJson(project) || hasAlpineWizardInScriptTags(project) || diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt index 778a9c7..0f27c5b 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt @@ -8,8 +8,15 @@ import com.intellij.util.xmlb.XmlSerializerUtil @State(name = "com.github.inxilpro.intellijalpine.AlpineProjectSettingsState", storages = [Storage("alpine-project.xml")]) class AlpineProjectSettingsState : PersistentStateComponent { - var enableAlpineAjax = false - var enableAlpineWizard = false + 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 diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt index b6c6c90..8ed9a6c 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -1,5 +1,6 @@ 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 @@ -11,8 +12,7 @@ class AlpineSettingsComponent(private val project: Project?) { val panel: JPanel private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons") - private val myEnableAlpineAjaxStatus = JBCheckBox("Enable alpine-ajax support for this project") - private val myEnableAlpineWizardStatus = JBCheckBox("Enable alpine-wizard support for this project") + private val pluginCheckBoxes = mutableMapOf() val preferredFocusedComponent: JComponent get() = myShowGutterIconsStatus @@ -23,17 +23,13 @@ class AlpineSettingsComponent(private val project: Project?) { myShowGutterIconsStatus.isSelected = newStatus } - var enableAlpineAjaxStatus: Boolean - get() = myEnableAlpineAjaxStatus.isSelected - set(newStatus) { - myEnableAlpineAjaxStatus.isSelected = newStatus - } + fun getPluginStatus(pluginName: String): Boolean { + return pluginCheckBoxes[pluginName]?.isSelected ?: false + } - var enableAlpineWizardStatus: Boolean - get() = myEnableAlpineWizardStatus.isSelected - set(newStatus) { - myEnableAlpineWizardStatus.isSelected = newStatus - } + fun setPluginStatus(pluginName: String, enabled: Boolean) { + pluginCheckBoxes[pluginName]?.isSelected = enabled + } init { val builder = FormBuilder.createFormBuilder() @@ -43,8 +39,13 @@ class AlpineSettingsComponent(private val project: Project?) { // Only show project settings if we have a project context if (project != null) { builder.addComponent(TitledSeparator("Project Settings")) - .addComponent(myEnableAlpineAjaxStatus, 1) - .addComponent(myEnableAlpineWizardStatus, 1) + + // Dynamically add checkboxes for each registered plugin + AlpinePluginRegistry.getInstance().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 diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt index d9806d3..0804f77 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt @@ -1,5 +1,6 @@ package com.github.inxilpro.intellijalpine.settings +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry import com.github.inxilpro.intellijalpine.settings.AlpineSettingsComponent import com.github.inxilpro.intellijalpine.settings.AlpineSettingsState import com.intellij.openapi.options.Configurable @@ -28,10 +29,15 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { // Check project settings if we have a project if (project != null) { - val projectSettings = AlpineProjectSettingsState.getInstance(project) - isModified = isModified || - mySettingsComponent?.enableAlpineAjaxStatus != projectSettings.enableAlpineAjax || - mySettingsComponent?.enableAlpineWizardStatus != projectSettings.enableAlpineWizard + val registry = AlpinePluginRegistry.getInstance() + 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 @@ -43,9 +49,16 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { // Apply project settings if we have a project if (project != null) { - val projectSettings = AlpineProjectSettingsState.getInstance(project) - projectSettings.enableAlpineAjax = mySettingsComponent?.enableAlpineAjaxStatus != false - projectSettings.enableAlpineWizard = mySettingsComponent?.enableAlpineWizardStatus != false + val registry = AlpinePluginRegistry.getInstance() + 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) + } + } } } @@ -55,9 +68,12 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { // Reset project settings if we have a project if (project != null) { - val projectSettings = AlpineProjectSettingsState.getInstance(project) - mySettingsComponent?.enableAlpineAjaxStatus = projectSettings.enableAlpineAjax - mySettingsComponent?.enableAlpineWizardStatus = projectSettings.enableAlpineWizard + val registry = AlpinePluginRegistry.getInstance() + registry.getRegisteredPlugins().forEach { plugin -> + val pluginName = plugin.getPluginName() + val enabled = registry.isPluginEnabled(project, pluginName) + mySettingsComponent?.setPluginStatus(pluginName, enabled) + } } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e656c32..5f2f690 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -33,7 +33,6 @@ - Date: Fri, 20 Jun 2025 18:35:20 -0400 Subject: [PATCH 15/32] wip --- .../attributes/AttributeInfo.kt | 7 +- .../attributes/AttributeUtil.kt | 65 ++++++++++--------- .../completion/AutoCompleteSuggestions.kt | 2 +- .../intellijalpine/core/AlpinePlugin.kt | 2 + .../plugins/AlpineAjaxPlugin.kt | 6 ++ 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 19ff7a7..84fd7fa 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -66,20 +66,21 @@ class AttributeInfo(val attribute: String) { } fun isDirective(): Boolean { + // FIXME: Handle plugin directives, too return AttributeUtil.directives.contains(name) } fun isTarget(): Boolean { - return "x-target:" == prefix // TODO: Move to plugin system (and in canBePrefix) + return "x-target:" == prefix } fun hasValue(): Boolean { - return "x-cloak" != name && "x-ignore" != name && "x-sync" != name + return "x-cloak" != name && "x-ignore" != name } fun canBePrefix(): Boolean { - return "x-bind" == name || "x-transition" == name || "x-on" == name || "x-target" == name + return "x-bind" == name || "x-transition" == name || "x-on" == name } @Suppress("ReturnCount") diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index 59aa710..3430cc2 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -1,10 +1,8 @@ package com.github.inxilpro.intellijalpine.attributes import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry -import com.github.inxilpro.intellijalpine.attributes.AttributeInfo import com.github.inxilpro.intellijalpine.support.LanguageUtil import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile import com.intellij.psi.html.HtmlTag import com.intellij.psi.impl.source.html.dtd.HtmlAttributeDescriptorImpl import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl @@ -24,9 +22,9 @@ object AttributeUtil { ) val xmlPrefixes: Array - get() = coreXmlPrefixes // TODO: Merge in plugin prefixes + get() = coreXmlPrefixes - private val coreDirectives = arrayOf( + val directives = arrayOf( "x-data", "x-init", "x-show", @@ -51,27 +49,6 @@ object AttributeUtil { "x-spread", // deprecated ) - private val ajaxDirectives = arrayOf( - "x-target", - "x-headers", - "x-merge", - "x-autofocus", - "x-sync", - ) - - val directives: Array - get() = coreDirectives + ajaxDirectives - - fun getDirectivesForProject(project: Project, contextFile: PsiFile?): Array { - val pluginDirectives = AlpinePluginRegistry.Companion.getInstance().getAllDirectives(project) - return (coreDirectives.toList() + pluginDirectives).toTypedArray() - } - - fun getXmlPrefixesForProject(project: Project): Array { - val pluginPrefixes = AlpinePluginRegistry.Companion.getInstance().getAllPrefixes(project) - return (coreXmlPrefixes.toList() + pluginPrefixes).toTypedArray() - } - val templateDirectives = arrayOf( "x-if", "x-for", @@ -222,6 +199,16 @@ object AttributeUtil { Pair("wheel", "WheelEvent"), ) + fun getDirectivesForProject(project: Project): Array { + val pluginDirectives = AlpinePluginRegistry.Companion.getInstance().getAllDirectives(project) + return (directives.toList() + pluginDirectives).toTypedArray() + } + + fun getXmlPrefixesForProject(project: Project): Array { + val pluginPrefixes = AlpinePluginRegistry.Companion.getInstance().getAllPrefixes(project) + return (coreXmlPrefixes.toList() + pluginPrefixes).toTypedArray() + } + fun isXmlPrefix(prefix: String): Boolean { return xmlPrefixes.contains(prefix) } @@ -230,6 +217,7 @@ object AttributeUtil { return templateDirectives.contains(directive) } + // FIXME: Remove this or implement it fun getValidAttributesWithInfo(xmlTag: HtmlTag): Array { return validAttributes.getOrPut(xmlTag.name, { buildValidAttributes(xmlTag) }) } @@ -280,7 +268,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 } @@ -305,16 +293,31 @@ object AttributeUtil { return name.startsWith("x-") || name.startsWith("@") || name.startsWith(':') } - private fun shouldInjectJavaScript(name: String): Boolean { - // `x-target:dynamic` should still inject JavaScript, but plain x-target should not - if (name == "x-target") return false + 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 + } + - return !name.startsWith("x-transition:") && "x-mask" != name && "x-modelable" != name && "x-autofocus" != name && "x-sync" != name && "x-merge" != name + val enabledPlugins = AlpinePluginRegistry.getInstance().getEnabledPlugins(project) + for (plugin in enabledPlugins) { + val pluginDirectives = plugin.getDirectives() + val pluginPrefixes = plugin.getPrefixes() + + // If this attribute belongs to this plugin, let the plugin decide + if (pluginDirectives.contains(name) || pluginPrefixes.any { name.startsWith("$it:") }) { + return plugin.directiveSupportJavaScript(name) + } + } + + // For core attributes and unknown attributes, default to true (inject JS) + return true } private fun buildValidAttributes(htmlTag: HtmlTag): Array { val descriptors = mutableListOf() - val projectDirectives = getDirectivesForProject(htmlTag.project, htmlTag.containingFile) + val projectDirectives = getDirectivesForProject(htmlTag.project) for (directive in projectDirectives) { if (htmlTag.name != "template" && isTemplateDirective(directive)) { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt index b1a48aa..b9cfcce 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt @@ -24,7 +24,7 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } private fun addDirectives() { - for (directive in AttributeUtil.directives) { + for (directive in AttributeUtil.getDirectivesForProject(htmlTag.project)) { if (tagName != "template" && AttributeUtil.isTemplateDirective(directive)) { continue } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt index cc48043..da10297 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -21,6 +21,8 @@ interface AlpinePlugin { fun injectJsContext(context: MutablePair): MutablePair = context + fun directiveSupportJavaScript(directive: String): Boolean = true + fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {} fun getDirectives(): List = emptyList() diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index d20d47d..76cbca5 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -82,6 +82,12 @@ class AlpineAjaxPlugin : AlpinePlugin { 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")) From 09c321abdd64815a071191b96939c1daec611710 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:06:49 -0400 Subject: [PATCH 16/32] Better directive/prefix handling --- .../attributes/AttributeInfo.kt | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 84fd7fa..73c606b 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -4,6 +4,19 @@ import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry @Suppress("MemberVisibilityCanBePrivate") class AttributeInfo(val attribute: String) { + + companion object { + // Memoized values for all registered plugin directives and prefixes + private val pluginDirectives: List by lazy { + AlpinePluginRegistry.getInstance().getRegisteredPlugins() + .flatMap { it.getDirectives() } + } + + private val pluginPrefixes: List by lazy { + AlpinePluginRegistry.getInstance().getRegisteredPlugins() + .flatMap { it.getPrefixes() } + } + } private val typeTexts = hashMapOf( "x-data" to "New Alpine.js component scope", "x-init" to "Run on initialization", @@ -66,8 +79,7 @@ class AttributeInfo(val attribute: String) { } fun isDirective(): Boolean { - // FIXME: Handle plugin directives, too - return AttributeUtil.directives.contains(name) + return AttributeUtil.directives.contains(name) || pluginDirectives.contains(name) } @@ -80,7 +92,12 @@ class AttributeInfo(val attribute: String) { } fun canBePrefix(): Boolean { - return "x-bind" == name || "x-transition" == name || "x-on" == name + // Check core prefixes + if ("x-bind" == name || "x-transition" == name || "x-on" == name) { + return true + } + + return pluginPrefixes.contains(name) } @Suppress("ReturnCount") @@ -101,9 +118,11 @@ class AttributeInfo(val attribute: String) { return "x-transition:" } - - if (attribute.startsWith("x-target:")) { - return "x-target:" + // Check all registered plugin prefixes (regardless of enablement) + for (prefix in pluginPrefixes) { + if (attribute.startsWith("$prefix:")) { + return "$prefix:" + } } return "" From 730ed4ae78f5c7da962982f78056b42d8ddb6fb1 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:18:49 -0400 Subject: [PATCH 17/32] Fix how prefixes/etc are collected --- .../attributes/AttributeInfo.kt | 47 ++++--------------- .../attributes/AttributeUtil.kt | 27 +++++++---- 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 73c606b..92b253b 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -4,19 +4,7 @@ import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry @Suppress("MemberVisibilityCanBePrivate") class AttributeInfo(val attribute: String) { - - companion object { - // Memoized values for all registered plugin directives and prefixes - private val pluginDirectives: List by lazy { - AlpinePluginRegistry.getInstance().getRegisteredPlugins() - .flatMap { it.getDirectives() } - } - - private val pluginPrefixes: List by lazy { - AlpinePluginRegistry.getInstance().getRegisteredPlugins() - .flatMap { it.getPrefixes() } - } - } + private val typeTexts = hashMapOf( "x-data" to "New Alpine.js component scope", "x-init" to "Run on initialization", @@ -63,7 +51,7 @@ class AttributeInfo(val attribute: String) { @Suppress("ComplexCondition") fun isAlpine(): Boolean { - return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() || this.isTarget() + return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() } fun isEvent(): Boolean { @@ -79,12 +67,7 @@ class AttributeInfo(val attribute: String) { } fun isDirective(): Boolean { - return AttributeUtil.directives.contains(name) || pluginDirectives.contains(name) - } - - - fun isTarget(): Boolean { - return "x-target:" == prefix + return AttributeUtil.directives.contains(name) } fun hasValue(): Boolean { @@ -92,16 +75,17 @@ class AttributeInfo(val attribute: String) { } fun canBePrefix(): Boolean { - // Check core prefixes - if ("x-bind" == name || "x-transition" == name || "x-on" == name) { - return true - } - - return pluginPrefixes.contains(name) + return AttributeUtil.prefixes.contains(name) } @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 @@ -114,17 +98,6 @@ class AttributeInfo(val attribute: String) { } } - if (attribute.startsWith("x-transition:")) { - return "x-transition:" - } - - // Check all registered plugin prefixes (regardless of enablement) - for (prefix in pluginPrefixes) { - if (attribute.startsWith("$prefix:")) { - return "$prefix:" - } - } - return "" } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index 3430cc2..726e0e8 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -15,16 +15,20 @@ import com.intellij.xml.XmlAttributeDescriptor object AttributeUtil { private val validAttributes = mutableMapOf>() - private val coreXmlPrefixes = arrayOf( + private val corePrefixes = listOf( "x-on", "x-bind", "x-transition", ) - val xmlPrefixes: Array - get() = coreXmlPrefixes + val prefixes: List by lazy { + AlpinePluginRegistry.getInstance().getRegisteredPlugins() + .flatMap { it.getPrefixes() } + .union(corePrefixes) + .toList() + } - val directives = arrayOf( + private val coreDirectives = listOf( "x-data", "x-init", "x-show", @@ -49,6 +53,13 @@ object AttributeUtil { "x-spread", // deprecated ) + val directives: List by lazy { + AlpinePluginRegistry.getInstance().getRegisteredPlugins() + .flatMap { it.getDirectives() } + .union(coreDirectives) + .toList() + } + val templateDirectives = arrayOf( "x-if", "x-for", @@ -93,10 +104,6 @@ object AttributeUtil { "delay", ) - val numericModifiers = arrayOf( - "scale", - ) - val transitionModifiers = arrayOf( "duration", "delay", @@ -206,11 +213,11 @@ object AttributeUtil { fun getXmlPrefixesForProject(project: Project): Array { val pluginPrefixes = AlpinePluginRegistry.Companion.getInstance().getAllPrefixes(project) - return (coreXmlPrefixes.toList() + pluginPrefixes).toTypedArray() + return (prefixes.toList() + pluginPrefixes).toTypedArray() } fun isXmlPrefix(prefix: String): Boolean { - return xmlPrefixes.contains(prefix) + return prefixes.contains(prefix) } fun isTemplateDirective(directive: String): Boolean { From 236cf8033326be76883bba25d27b1210a22b1f4c Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:21:11 -0400 Subject: [PATCH 18/32] Just use canBePrefix --- .../github/inxilpro/intellijalpine/attributes/AttributeInfo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 92b253b..b079301 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -51,7 +51,7 @@ class AttributeInfo(val attribute: String) { @Suppress("ComplexCondition") fun isAlpine(): Boolean { - return this.isEvent() || this.isBound() || this.isTransition() || this.isDirective() + return this.isDirective() || this.canBePrefix() } fun isEvent(): Boolean { From 38c028188d6bfdf2f5497804d9e0dea0cf83162d Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:27:33 -0400 Subject: [PATCH 19/32] Fix Companion calls --- .../inxilpro/intellijalpine/attributes/AttributeUtil.kt | 4 ++-- .../injection/AlpineJavaScriptAttributeValueInjector.kt | 2 +- .../inxilpro/intellijalpine/settings/AlpineProjectActivity.kt | 2 +- .../inxilpro/intellijalpine/settings/AlpineProjectListener.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index 726e0e8..1616cca 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -207,12 +207,12 @@ object AttributeUtil { ) fun getDirectivesForProject(project: Project): Array { - val pluginDirectives = AlpinePluginRegistry.Companion.getInstance().getAllDirectives(project) + val pluginDirectives = AlpinePluginRegistry.getInstance().getAllDirectives(project) return (directives.toList() + pluginDirectives).toTypedArray() } fun getXmlPrefixesForProject(project: Project): Array { - val pluginPrefixes = AlpinePluginRegistry.Companion.getInstance().getAllPrefixes(project) + val pluginPrefixes = AlpinePluginRegistry.getInstance().getAllPrefixes(project) return (prefixes.toList() + pluginPrefixes).toTypedArray() } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt index 524a770..c9c9383 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt @@ -151,7 +151,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { val globalContext = MutablePair(globalMagics, ""); - val context = AlpinePluginRegistry.Companion.getInstance().injectAllJsContext(host.project, globalContext) + val context = AlpinePluginRegistry.getInstance().injectAllJsContext(host.project, globalContext) if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt index 985adae..579c289 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt @@ -8,7 +8,7 @@ import com.intellij.openapi.startup.ProjectActivity class AlpineProjectActivity : ProjectActivity { override suspend fun execute(project: Project) { readAction { - AlpinePluginRegistry.Companion.getInstance().checkAndAutoEnablePlugins(project) + AlpinePluginRegistry.getInstance().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 index dfe8fc2..aee0ba1 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt @@ -6,6 +6,6 @@ import com.intellij.openapi.project.ProjectManagerListener class AlpineProjectListener : ProjectManagerListener { override fun projectClosed(project: Project) { - AlpinePluginRegistry.Companion.getInstance().cleanup(project) + AlpinePluginRegistry.getInstance().cleanup(project) } } \ No newline at end of file From fb57b60a62afc3e031fd4df19e1a5b86ff648d6c Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:31:02 -0400 Subject: [PATCH 20/32] Remove dead getValidAttributesWithInfo --- .../attributes/AttributeUtil.kt | 46 +------------------ 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index 1616cca..c81515f 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -13,8 +13,6 @@ import com.intellij.psi.xml.XmlTag import com.intellij.xml.XmlAttributeDescriptor object AttributeUtil { - private val validAttributes = mutableMapOf>() - private val corePrefixes = listOf( "x-on", "x-bind", @@ -224,11 +222,6 @@ object AttributeUtil { return templateDirectives.contains(directive) } - // FIXME: Remove this or implement it - 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)) { @@ -311,49 +304,14 @@ object AttributeUtil { for (plugin in enabledPlugins) { val pluginDirectives = plugin.getDirectives() val pluginPrefixes = plugin.getPrefixes() - + // If this attribute belongs to this plugin, let the plugin decide if (pluginDirectives.contains(name) || pluginPrefixes.any { name.startsWith("$it:") }) { return plugin.directiveSupportJavaScript(name) } } - + // For core attributes and unknown attributes, default to true (inject JS) return true } - - private fun buildValidAttributes(htmlTag: HtmlTag): Array { - val descriptors = mutableListOf() - val projectDirectives = getDirectivesForProject(htmlTag.project) - - for (directive in projectDirectives) { - if (htmlTag.name != "template" && isTemplateDirective(directive)) { - continue - } - - descriptors.add(AttributeInfo(directive)) - } - - 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)) - } - } - } - - 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() - } } \ No newline at end of file From bbcb9be7d583d9397f03ae8094d941a85a3cc7f4 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:37:09 -0400 Subject: [PATCH 21/32] getInstance() -> instance --- .../intellijalpine/attributes/AttributeInfo.kt | 2 +- .../intellijalpine/attributes/AttributeUtil.kt | 10 +++++----- .../completion/AutoCompleteSuggestions.kt | 2 +- .../intellijalpine/core/AlpineLineMarkerProvider.kt | 2 +- .../intellijalpine/core/AlpinePluginRegistry.kt | 6 ++---- .../AlpineJavaScriptAttributeValueInjector.kt | 2 +- .../intellijalpine/settings/AlpineProjectActivity.kt | 2 +- .../intellijalpine/settings/AlpineProjectListener.kt | 2 +- .../settings/AlpineSettingsComponent.kt | 2 +- .../settings/AlpineSettingsConfigurable.kt | 12 ++++++------ 10 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index b079301..794a153 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -116,7 +116,7 @@ class AttributeInfo(val attribute: String) { } // First check plugin registry for type text - val pluginTypeText = AlpinePluginRegistry.Companion.getInstance().getTypeText(this) + val pluginTypeText = AlpinePluginRegistry.instance.getTypeText(this) if (pluginTypeText != null) { return pluginTypeText } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index c81515f..e9d76bb 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -20,7 +20,7 @@ object AttributeUtil { ) val prefixes: List by lazy { - AlpinePluginRegistry.getInstance().getRegisteredPlugins() + AlpinePluginRegistry.instance.getRegisteredPlugins() .flatMap { it.getPrefixes() } .union(corePrefixes) .toList() @@ -52,7 +52,7 @@ object AttributeUtil { ) val directives: List by lazy { - AlpinePluginRegistry.getInstance().getRegisteredPlugins() + AlpinePluginRegistry.instance.getRegisteredPlugins() .flatMap { it.getDirectives() } .union(coreDirectives) .toList() @@ -205,12 +205,12 @@ object AttributeUtil { ) fun getDirectivesForProject(project: Project): Array { - val pluginDirectives = AlpinePluginRegistry.getInstance().getAllDirectives(project) + val pluginDirectives = AlpinePluginRegistry.instance.getAllDirectives(project) return (directives.toList() + pluginDirectives).toTypedArray() } fun getXmlPrefixesForProject(project: Project): Array { - val pluginPrefixes = AlpinePluginRegistry.getInstance().getAllPrefixes(project) + val pluginPrefixes = AlpinePluginRegistry.instance.getAllPrefixes(project) return (prefixes.toList() + pluginPrefixes).toTypedArray() } @@ -300,7 +300,7 @@ object AttributeUtil { } - val enabledPlugins = AlpinePluginRegistry.getInstance().getEnabledPlugins(project) + val enabledPlugins = AlpinePluginRegistry.instance.getEnabledPlugins(project) for (plugin in enabledPlugins) { val pluginDirectives = plugin.getDirectives() val pluginPrefixes = plugin.getPrefixes() diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt index b9cfcce..43cb9be 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt @@ -80,7 +80,7 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } private fun addPlugins() { - AlpinePluginRegistry.getInstance().injectAllAutoCompleteSuggestions(htmlTag.project, this) + AlpinePluginRegistry.instance.injectAllAutoCompleteSuggestions(htmlTag.project, this) } private fun addEvent(descriptor: XmlAttributeDescriptor) { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt index 81362cb..95f10b8 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpineLineMarkerProvider.kt @@ -21,7 +21,7 @@ class AlpineLineMarkerProvider : RelatedItemLineMarkerProvider() { element: PsiElement, result: MutableCollection?> ) { - if (!AlpineSettingsState.Companion.instance.showGutterIcons) return + if (!AlpineSettingsState.instance.showGutterIcons) return if (element is XmlAttribute && element.descriptor is AlpineAttributeDescriptor) { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt index 27ea1e4..64549e2 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -18,10 +18,8 @@ class AlpinePluginRegistry { private val listeners = ConcurrentHashMap() companion object { - fun getInstance(): AlpinePluginRegistry { - return ApplicationManager.getApplication() - .getService(AlpinePluginRegistry::class.java) - } + val instance: AlpinePluginRegistry + get() = ApplicationManager.getApplication().getService(AlpinePluginRegistry::class.java) } fun getRegisteredPlugins(): List { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt index c9c9383..197d585 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt @@ -151,7 +151,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { val globalContext = MutablePair(globalMagics, ""); - val context = AlpinePluginRegistry.getInstance().injectAllJsContext(host.project, globalContext) + val context = AlpinePluginRegistry.instance.injectAllJsContext(host.project, globalContext) if ("x-data" != directive) { context.left = addTypingToCoreMagics(host) + context.left diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt index 579c289..22d6456 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectActivity.kt @@ -8,7 +8,7 @@ import com.intellij.openapi.startup.ProjectActivity class AlpineProjectActivity : ProjectActivity { override suspend fun execute(project: Project) { readAction { - AlpinePluginRegistry.getInstance().checkAndAutoEnablePlugins(project) + 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 index aee0ba1..0d013f3 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectListener.kt @@ -6,6 +6,6 @@ import com.intellij.openapi.project.ProjectManagerListener class AlpineProjectListener : ProjectManagerListener { override fun projectClosed(project: Project) { - AlpinePluginRegistry.getInstance().cleanup(project) + AlpinePluginRegistry.instance.cleanup(project) } } \ 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 index 8ed9a6c..bff244c 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -41,7 +41,7 @@ class AlpineSettingsComponent(private val project: Project?) { builder.addComponent(TitledSeparator("Project Settings")) // Dynamically add checkboxes for each registered plugin - AlpinePluginRegistry.getInstance().getRegisteredPlugins().forEach { plugin -> + AlpinePluginRegistry.instance.getRegisteredPlugins().forEach { plugin -> val checkBox = JBCheckBox("Enable ${plugin.getPackageDisplayName()} support for this project") pluginCheckBoxes[plugin.getPluginName()] = checkBox builder.addComponent(checkBox, 1) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt index 0804f77..9c4ec20 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt @@ -24,12 +24,12 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { } override fun isModified(): Boolean { - val appSettings = AlpineSettingsState.Companion.instance + val appSettings = AlpineSettingsState.instance var isModified = mySettingsComponent?.showGutterIconsStatus != appSettings.showGutterIcons // Check project settings if we have a project if (project != null) { - val registry = AlpinePluginRegistry.getInstance() + val registry = AlpinePluginRegistry.instance registry.getRegisteredPlugins().forEach { plugin -> val pluginName = plugin.getPluginName() val currentStatus = mySettingsComponent?.getPluginStatus(pluginName) ?: false @@ -44,12 +44,12 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { } override fun apply() { - val appSettings = AlpineSettingsState.Companion.instance + val appSettings = AlpineSettingsState.instance appSettings.showGutterIcons = mySettingsComponent?.showGutterIconsStatus != false // Apply project settings if we have a project if (project != null) { - val registry = AlpinePluginRegistry.getInstance() + val registry = AlpinePluginRegistry.instance registry.getRegisteredPlugins().forEach { plugin -> val pluginName = plugin.getPluginName() val enabled = mySettingsComponent?.getPluginStatus(pluginName) ?: false @@ -63,12 +63,12 @@ class AlpineSettingsConfigurable(private val project: Project?) : Configurable { } override fun reset() { - val appSettings = AlpineSettingsState.Companion.instance + val appSettings = AlpineSettingsState.instance mySettingsComponent?.showGutterIconsStatus = appSettings.showGutterIcons // Reset project settings if we have a project if (project != null) { - val registry = AlpinePluginRegistry.getInstance() + val registry = AlpinePluginRegistry.instance registry.getRegisteredPlugins().forEach { plugin -> val pluginName = plugin.getPluginName() val enabled = registry.isPluginEnabled(project, pluginName) From 9d23b6e7391b10dbbea2d22d10d3938b2565cfad Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:41:22 -0400 Subject: [PATCH 22/32] Change settings file name --- .../intellijalpine/settings/AlpineProjectSettingsState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt index 0f27c5b..bed9a6e 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt @@ -6,7 +6,7 @@ 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("alpine-project.xml")]) +@State(name = "com.github.inxilpro.intellijalpine.AlpineProjectSettingsState", storages = [Storage("alpinejs-support.xml")]) class AlpineProjectSettingsState : PersistentStateComponent { var enabledPlugins = mutableMapOf() From 923e6d679c828f962e2cc1eabb6ce06ac6d2e562 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:44:42 -0400 Subject: [PATCH 23/32] New Claude instructions --- CLAUDE.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3cbdc3e..ac51a20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,11 @@ 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()` + ## Commands ### Building & Running @@ -25,14 +30,7 @@ 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 - -### Key Components - -1. **AttributesProvider** - Central class that provides Alpine attribute descriptors to the IDE's HTML/XML support system. It defines which Alpine attributes are available and their properties. -2. **AlpineJavaScriptAttributeValueInjector** - Injects JavaScript language into Alpine attribute values, enabling proper syntax highlighting and code completion within attributes like `x-data` and `x-show`. -3. **AlpineCompletionContributor** - Handles auto-completion logic for Alpine directives, providing context-aware suggestions based on cursor position. -4. **AlpineTargetReferenceContributor** - Provides native IntelliJ reference support for `x-target` attributes, enabling go-to-definition, find usages, refactoring, and error highlighting for ID references. -5. **AttributeInfo** - Contains all Alpine directive definitions and metadata, including documentation and allowed contexts for each directive. +- Plugin support for third-party alpine plugins ### Plugin Configuration @@ -44,9 +42,9 @@ The plugin is configured via: The plugin requires: -- IntelliJ IDEA 2023.1 or newer +- IntelliJ IDEA 2025.1 or newer - JavaScript and HtmlTools plugins as dependencies -- Java 17 runtime +- Java 21 runtime ### Release Process From 6e3283d14e9672601480940798a15db88ede9ab8 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:51:29 -0400 Subject: [PATCH 24/32] Better settings --- .../settings/AlpineSettingsComponent.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt index bff244c..7b8b8a4 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -4,7 +4,9 @@ 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 @@ -33,16 +35,23 @@ class AlpineSettingsComponent(private val project: Project?) { init { val builder = FormBuilder.createFormBuilder() - .addComponent(TitledSeparator("Application Settings")) + .addComponent(TitledSeparator("Plugin Settings")) .addComponent(myShowGutterIconsStatus, 1) // Only show project settings if we have a project context if (project != null) { - builder.addComponent(TitledSeparator("Project Settings")) + 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") + val checkBox = JBCheckBox("Enable “${plugin.getPackageDisplayName()}” support for this project") pluginCheckBoxes[plugin.getPluginName()] = checkBox builder.addComponent(checkBox, 1) } From 836e899426a4fabfd63767564712afd990298701 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 19:53:09 -0400 Subject: [PATCH 25/32] Update LanguageUtil.kt --- .../github/inxilpro/intellijalpine/support/LanguageUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt index 596825f..fdc85aa 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt @@ -36,11 +36,11 @@ object LanguageUtil { } private fun isHtmlBasedLanguage(language: Language): Boolean { - if (language.isKindOf(HTMLLanguage.Companion.INSTANCE)) { + if (language.isKindOf(HTMLLanguage.INSTANCE)) { return true } - if (language.isKindOf(XMLLanguage.Companion.INSTANCE)) { + if (language.isKindOf(XMLLanguage.INSTANCE)) { return TEMPLATE_LANGUAGE_IDS.contains(language.id) } From 61763ef9f9d0f35795c19876118865414437404e Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 20:39:24 -0400 Subject: [PATCH 26/32] Better completion contributor handling for plugins --- CLAUDE.md | 1 + .../completion/AlpineCompletionContributor.kt | 19 +++++++----- .../AlpinePluginCompletionProviderWrapper.kt | 29 +++++++++++++++++++ .../intellijalpine/core/AlpinePlugin.kt | 2 ++ .../core/AlpinePluginRegistry.kt | 4 +++ .../core/CompletionProviderRegistration.kt | 12 ++++++++ .../plugins/AlpineAjaxPlugin.kt | 14 +++++++++ .../AlpineMergeValueCompletionProvider.kt | 9 ++++-- 8 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/core/CompletionProviderRegistration.kt diff --git a/CLAUDE.md b/CLAUDE.md index ac51a20..acf76bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - 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 diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt index 61df83f..fc9615a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineCompletionContributor.kt @@ -1,6 +1,6 @@ package com.github.inxilpro.intellijalpine.completion -import com.github.inxilpro.intellijalpine.plugins.AlpineMergeValueCompletionProvider +import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionType import com.intellij.patterns.PlatformPatterns @@ -16,12 +16,15 @@ class AlpineCompletionContributor : CompletionContributor() { AlpineAttributeCompletionProvider() ) - // Attribute value completion for x-merge - extend( - CompletionType.BASIC, - PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) - .inside(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("x-merge"))), - AlpineMergeValueCompletionProvider() - ) + // 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..b5216d3 --- /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/core/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt index da10297..a7f8afa 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -24,6 +24,8 @@ interface AlpinePlugin { fun directiveSupportJavaScript(directive: String): Boolean = true fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {} + + fun getCompletionProviders(): List = emptyList() fun getDirectives(): List = emptyList() diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt index 64549e2..be8f309 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -35,6 +35,10 @@ class AlpinePluginRegistry { 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) } 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/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index 76cbca5..22acd9f 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -1,6 +1,7 @@ package com.github.inxilpro.intellijalpine.plugins import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration import com.github.inxilpro.intellijalpine.settings.AlpineProjectSettingsState import com.github.inxilpro.intellijalpine.attributes.AttributeInfo import com.github.inxilpro.intellijalpine.attributes.AttributeUtil @@ -9,12 +10,15 @@ import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonObject import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.XmlPatterns 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 +import com.intellij.psi.xml.XmlTokenType import org.apache.commons.lang3.tuple.MutablePair class AlpineAjaxPlugin : AlpinePlugin { @@ -95,6 +99,16 @@ class AlpineAjaxPlugin : AlpinePlugin { 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", diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt index 12a92c3..728bf5a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineMergeValueCompletionProvider.kt @@ -1,15 +1,18 @@ 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.CompletionProvider 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 : CompletionProvider() { +class AlpineMergeValueCompletionProvider( + plugin: AlpinePlugin +) : AlpinePluginCompletionProvider(plugin) { private val mergeStrategies = arrayOf( "before" to "Insert content before target", @@ -21,7 +24,7 @@ class AlpineMergeValueCompletionProvider : CompletionProvider Date: Fri, 20 Jun 2025 20:52:03 -0400 Subject: [PATCH 27/32] Centralize package.json checks --- .../core/AlpinePluginRegistry.kt | 41 +++++++++++++++++-- .../plugins/AlpineAjaxPlugin.kt | 34 +-------------- .../plugins/AlpineWizardPlugin.kt | 33 +-------------- 3 files changed, 39 insertions(+), 69 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt index be8f309..fba5d4a 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -3,12 +3,17 @@ 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.settings.AlpineProjectSettingsState +import com.intellij.json.psi.JsonFile +import com.intellij.json.psi.JsonObject 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.psi.PsiManager +import com.intellij.psi.search.FilenameIndex +import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.messages.MessageBusConnection import org.apache.commons.lang3.tuple.MutablePair import java.util.concurrent.ConcurrentHashMap @@ -71,15 +76,44 @@ class AlpinePluginRegistry { fun checkAndAutoEnablePlugins(project: Project) { getRegisteredPlugins().forEach { plugin -> - if (!isPluginEnabled(project, plugin.getPluginName()) && plugin.performDetection(project)) { + if (!isPluginEnabled(project, plugin.getPluginName()) && isPluginDetected(project, plugin)) { enablePlugin(project, plugin.getPluginName()) } } - // Set up package.json listener for auto-enabling plugins setupPackageJsonListener(project) } + private fun isPluginDetected(project: Project, plugin: AlpinePlugin): Boolean { + return hasPluginInPackageJson(project, plugin) || plugin.performDetection(project) + } + + private fun hasPluginInPackageJson(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 + } + } + private fun setupPackageJsonListener(project: Project) { val projectPath = project.basePath ?: project.name @@ -99,10 +133,9 @@ class AlpinePluginRegistry { } if (hasPackageJsonChanges) { - // Re-check and potentially auto-enable plugins on package.json changes ApplicationManager.getApplication().runReadAction { getRegisteredPlugins().forEach { plugin -> - if (!isPluginEnabled(project, plugin.getPluginName()) && plugin.performDetection(project)) { + if (!isPluginEnabled(project, plugin.getPluginName()) && isPluginDetected(project, plugin)) { enablePlugin(project, plugin.getPluginName()) } } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index 22acd9f..4e09bc7 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -2,12 +2,8 @@ package com.github.inxilpro.intellijalpine.plugins import com.github.inxilpro.intellijalpine.core.AlpinePlugin import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration -import com.github.inxilpro.intellijalpine.settings.AlpineProjectSettingsState import com.github.inxilpro.intellijalpine.attributes.AttributeInfo -import com.github.inxilpro.intellijalpine.attributes.AttributeUtil import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions -import com.intellij.json.psi.JsonFile -import com.intellij.json.psi.JsonObject import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.patterns.PlatformPatterns @@ -122,35 +118,7 @@ class AlpineAjaxPlugin : AlpinePlugin { ) override fun performDetection(project: Project): Boolean { - return hasAlpineAjaxInPackageJson(project) || - hasAlpineAjaxInScriptTags(project) || - hasAlpineAjaxCode(project) - } - - private fun hasAlpineAjaxInPackageJson(project: Project): 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 - - hasAlpineAjaxDependency(dependencies) || hasAlpineAjaxDependency(devDependencies) - } else { - false - } - } - } - - private fun hasAlpineAjaxDependency(dependencies: JsonObject?): Boolean { - return getPackageNamesForDetection().any { packageName -> - dependencies?.findProperty(packageName) != null - } + return hasAlpineAjaxInScriptTags(project) || hasAlpineAjaxCode(project) } private fun hasAlpineAjaxInScriptTags(project: Project): Boolean { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt index cab9d27..86eb976 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt @@ -3,9 +3,6 @@ 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.settings.AlpineProjectSettingsState -import com.intellij.json.psi.JsonFile -import com.intellij.json.psi.JsonObject import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiManager @@ -111,35 +108,7 @@ class AlpineWizardPlugin : AlpinePlugin { ) override fun performDetection(project: Project): Boolean { - return hasAlpineWizardInPackageJson(project) || - hasAlpineWizardInScriptTags(project) || - hasAlpineWizardCode(project) - } - - private fun hasAlpineWizardInPackageJson(project: Project): 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 - - hasAlpineWizardDependency(dependencies) || hasAlpineWizardDependency(devDependencies) - } else { - false - } - } - } - - private fun hasAlpineWizardDependency(dependencies: JsonObject?): Boolean { - return getPackageNamesForDetection().any { packageName -> - dependencies?.findProperty(packageName) != null - } + return hasAlpineWizardInScriptTags(project) || hasAlpineWizardCode(project) } private fun hasAlpineWizardInScriptTags(project: Project): Boolean { From f5088863df516e1041d69152d32f4b7121d214bc Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 21:16:34 -0400 Subject: [PATCH 28/32] Better detection --- .../core/AlpinePluginRegistry.kt | 41 +----- .../core/detection/DetectionStrategy.kt | 8 ++ .../core/detection/PackageJsonDetector.kt | 37 ++++++ .../core/detection/PluginDetector.kt | 15 +++ .../core/detection/ScriptReferenceDetector.kt | 102 ++++++++++++++ .../plugins/AlpineAjaxPlugin.kt | 124 +----------------- .../plugins/AlpineWizardPlugin.kt | 112 ---------------- 7 files changed, 169 insertions(+), 270 deletions(-) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/DetectionStrategy.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PackageJsonDetector.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt index fba5d4a..353489b 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePluginRegistry.kt @@ -2,18 +2,14 @@ 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.json.psi.JsonFile -import com.intellij.json.psi.JsonObject 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.psi.PsiManager -import com.intellij.psi.search.FilenameIndex -import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.messages.MessageBusConnection import org.apache.commons.lang3.tuple.MutablePair import java.util.concurrent.ConcurrentHashMap @@ -21,6 +17,7 @@ import java.util.concurrent.ConcurrentHashMap @Service(Service.Level.APP) class AlpinePluginRegistry { private val listeners = ConcurrentHashMap() + private val detector = PluginDetector() companion object { val instance: AlpinePluginRegistry @@ -76,7 +73,7 @@ class AlpinePluginRegistry { fun checkAndAutoEnablePlugins(project: Project) { getRegisteredPlugins().forEach { plugin -> - if (!isPluginEnabled(project, plugin.getPluginName()) && isPluginDetected(project, plugin)) { + if (!isPluginEnabled(project, plugin.getPluginName()) && detector.detect(project, plugin)) { enablePlugin(project, plugin.getPluginName()) } } @@ -84,36 +81,6 @@ class AlpinePluginRegistry { setupPackageJsonListener(project) } - private fun isPluginDetected(project: Project, plugin: AlpinePlugin): Boolean { - return hasPluginInPackageJson(project, plugin) || plugin.performDetection(project) - } - - private fun hasPluginInPackageJson(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 - } - } - private fun setupPackageJsonListener(project: Project) { val projectPath = project.basePath ?: project.name @@ -135,7 +102,7 @@ class AlpinePluginRegistry { if (hasPackageJsonChanges) { ApplicationManager.getApplication().runReadAction { getRegisteredPlugins().forEach { plugin -> - if (!isPluginEnabled(project, plugin.getPluginName()) && isPluginDetected(project, plugin)) { + if (!isPluginEnabled(project, plugin.getPluginName()) && detector.detect(project, plugin)) { enablePlugin(project, plugin.getPluginName()) } } 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..7e49476 --- /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..1daf1b3 --- /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/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index 4e09bc7..027bc64 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -99,7 +99,9 @@ class AlpineAjaxPlugin : AlpinePlugin { return listOf( CompletionProviderRegistration( XmlPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) - .withParent(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("x-merge"))), + .withParent( + XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("x-merge")) + ), AlpineMergeValueCompletionProvider(this) ) ) @@ -116,124 +118,4 @@ class AlpineAjaxPlugin : AlpinePlugin { override fun getPrefixes(): List = listOf( "x-target" ) - - override fun performDetection(project: Project): Boolean { - return hasAlpineAjaxInScriptTags(project) || hasAlpineAjaxCode(project) - } - - private fun hasAlpineAjaxInScriptTags(project: Project): Boolean { - val htmlFiles = mutableListOf() - - // Get files by extension - val extensions = listOf("html", "htm", "php", "twig") - 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 { - psiFile is XmlFile -> { - // Handle HTML and XML files - val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) - .filter { it.name.equals("script", ignoreCase = true) } - - scriptTags.any { scriptTag -> - val src = scriptTag.getAttributeValue("src") - if (src != null) { - isAlpineAjaxScriptSrc(src) - } else { - hasAlpineAjaxPatterns(scriptTag.value.text) - } - } - } - virtualFile.name.endsWith(".blade.php") -> { - // Handle Blade files by checking their raw content - try { - val content = String(virtualFile.contentsToByteArray()) - hasAlpineAjaxPatterns(content) || hasAlpineAjaxScriptTagsInContent(content) - } catch (_: Exception) { - false - } - } - else -> false - } - } - } - - private fun isAlpineAjaxScriptSrc(src: String): Boolean { - return src.contains("alpine-ajax", ignoreCase = true) - } - - private fun hasAlpineAjaxScriptTagsInContent(content: String): Boolean { - // Use regex to find script tags with alpine-ajax references in raw HTML content - val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) - - return scriptTagRegex.findAll(content).any { match -> - val src = match.groupValues[1] - isAlpineAjaxScriptSrc(src) - } - } - - private fun hasAlpineAjaxCode(project: Project): Boolean { - val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") - 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()) - hasAlpineAjaxPatterns(content) - } catch (_: Exception) { - false - } - } - } - - private fun hasAlpineAjaxPatterns(content: String): Boolean { - val alpineAjaxPatterns = listOf( - // Import statements - "import.*alpine-ajax", - "from.*alpine-ajax", - "require.*alpine-ajax", - - // Alpine.js plugin registration - "Alpine\\.plugin\\s*\\(.*ajax", - "alpine\\.plugin\\s*\\(.*ajax", - - // Ajax-specific functions from the alpine-ajax source - "AjaxInterceptor", - "AjaxCommand", - "ajaxCommand", - "processAjaxResponse", - "handleAjaxRequest", - - // Magic helper usage patterns - "\\\$ajax\\s*\\(", - "x-target", - "x-swap", - "x-headers", - "x-replace" - ) - - return alpineAjaxPatterns.any { pattern -> - Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) - } - } } \ 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 index 86eb976..96443db 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt @@ -106,116 +106,4 @@ class AlpineWizardPlugin : AlpinePlugin { override fun getPrefixes(): List = listOf( "x-wizard" ) - - override fun performDetection(project: Project): Boolean { - return hasAlpineWizardInScriptTags(project) || hasAlpineWizardCode(project) - } - - private fun hasAlpineWizardInScriptTags(project: Project): Boolean { - val htmlFiles = mutableListOf() - - // Get files by extension - val extensions = listOf("html", "htm", "php", "twig") - 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 { - psiFile is XmlFile -> { - // Handle HTML and XML files - val scriptTags = PsiTreeUtil.collectElementsOfType(psiFile, XmlTag::class.java) - .filter { it.name.equals("script", ignoreCase = true) } - - scriptTags.any { scriptTag -> - val src = scriptTag.getAttributeValue("src") - if (src != null) { - isAlpineWizardScriptSrc(src) - } else { - hasAlpineWizardPatterns(scriptTag.value.text) - } - } - } - virtualFile.name.endsWith(".blade.php") -> { - // Handle Blade files by checking their raw content - try { - val content = String(virtualFile.contentsToByteArray()) - hasAlpineWizardPatterns(content) || hasAlpineWizardScriptTagsInContent(content) - } catch (_: Exception) { - false - } - } - else -> false - } - } - } - - private fun isAlpineWizardScriptSrc(src: String): Boolean { - return src.contains("alpine-wizard", ignoreCase = true) - } - - private fun hasAlpineWizardScriptTagsInContent(content: String): Boolean { - // Use regex to find script tags with alpine-wizard references in raw HTML content - val scriptTagRegex = Regex("]*src=['\"]([^'\"]*)['\"][^>]*>", RegexOption.IGNORE_CASE) - - return scriptTagRegex.findAll(content).any { match -> - val src = match.groupValues[1] - isAlpineWizardScriptSrc(src) - } - } - - private fun hasAlpineWizardCode(project: Project): Boolean { - val jsExtensions = listOf("js", "ts", "mjs", "jsx", "tsx") - 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()) - hasAlpineWizardPatterns(content) - } catch (_: Exception) { - false - } - } - } - - private fun hasAlpineWizardPatterns(content: String): Boolean { - val alpineWizardPatterns = listOf( - // Import statements - "import.*alpine-wizard", - "from.*alpine-wizard", - "require.*alpine-wizard", - - // Alpine.js plugin registration - "Alpine\\.plugin\\s*\\(.*wizard", - "alpine\\.plugin\\s*\\(.*wizard", - - // Wizard-specific usage patterns - "\\\$wizard\\s*\\.", - "x-wizard:", - "Alpine\\.wizard", - "alpine\\.wizard" - ) - - return alpineWizardPatterns.any { pattern -> - Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(content) - } - } } \ No newline at end of file From 390b6ea05ce011f1cd61d3b3fd0731f26d5e83c0 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 21:25:12 -0400 Subject: [PATCH 29/32] Bugfix --- .../attributes/AttributeInfo.kt | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 794a153..13ba8ec 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -50,33 +50,21 @@ class AttributeInfo(val attribute: String) { } @Suppress("ComplexCondition") - fun isAlpine(): Boolean { - return this.isDirective() || this.canBePrefix() - } + 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 hasValue(): Boolean { - return "x-cloak" != name && "x-ignore" != name - } + fun hasValue(): Boolean = "x-cloak" != name && "x-ignore" != name - fun canBePrefix(): Boolean { - return AttributeUtil.prefixes.contains(name) - } + fun canBePrefix(): Boolean = AttributeUtil.prefixes.contains(name) + + fun isPrefixed(): Boolean = prefix != "" @Suppress("ReturnCount") private fun extractPrefix(): String { From db82a9edeee6ca0fdb8e69eae976012950b3bdd7 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 21:39:04 -0400 Subject: [PATCH 30/32] Add basic alpine-tooltip support --- .../completion/AutoCompleteSuggestions.kt | 4 + .../intellijalpine/plugins/TooltipPlugin.kt | 76 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 3 files changed, 81 insertions(+) create mode 100644 src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt index 43cb9be..a8c9eb5 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt @@ -102,6 +102,10 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } } + fun addModifiers(modifiableDirective: String, modifiers: List) { + addModifiers(modifiableDirective, modifiers.toTypedArray()) + } + fun addModifiers(modifiableDirective: String, modifiers: Array) { if (!partialAttribute.startsWith(modifiableDirective)) { return 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..b9f49f7 --- /dev/null +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt @@ -0,0 +1,76 @@ +package com.github.inxilpro.intellijalpine.plugins + +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration +import com.github.inxilpro.intellijalpine.attributes.AttributeInfo +import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.XmlPatterns +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 +import com.intellij.psi.xml.XmlTokenType +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/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5f2f690..eefa1cb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -43,6 +43,7 @@ + From 2a617159d75615302b334c2c04056de27a2706ad Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 21:43:59 -0400 Subject: [PATCH 31/32] Clean up --- CHANGELOG.md | 8 ++++++++ .../attributes/AlpineAttributeDescriptor.kt | 1 - .../intellijalpine/attributes/AttributeInfo.kt | 2 +- .../intellijalpine/attributes/AttributeUtil.kt | 4 ---- .../AlpineAttributeCompletionProvider.kt | 1 - .../AlpinePluginCompletionProviderWrapper.kt | 2 +- .../inxilpro/intellijalpine/core/AlpinePlugin.kt | 5 +++-- .../core/detection/PluginDetector.kt | 4 ++-- .../core/detection/ScriptReferenceDetector.kt | 2 +- .../AlpineJavaScriptAttributeValueInjector.kt | 4 ++-- .../intellijalpine/plugins/AlpineAjaxPlugin.kt | 13 ++----------- .../plugins/AlpineTargetReferenceContributor.kt | 9 +++++++-- .../intellijalpine/plugins/AlpineWizardPlugin.kt | 8 -------- .../intellijalpine/plugins/TooltipPlugin.kt | 14 +------------- .../settings/AlpineProjectSettingsState.kt | 5 ++++- .../settings/AlpineSettingsComponent.kt | 4 ++-- .../settings/AlpineSettingsConfigurable.kt | 2 -- .../intellijalpine/support/LanguageUtil.kt | 4 +++- .../intellijalpine/support/XmlExtension.kt | 1 - src/main/resources/META-INF/plugin.xml | 15 +++++++++------ src/main/resources/META-INF/pluginIcon.svg | 7 ++++--- src/main/resources/alpineicon.svg | 7 ++++--- 22 files changed, 54 insertions(+), 68 deletions(-) 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/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt index 3382cb0..c37ab8d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AlpineAttributeDescriptor.kt @@ -1,7 +1,6 @@ package com.github.inxilpro.intellijalpine.attributes import com.github.inxilpro.intellijalpine.Alpine -import com.github.inxilpro.intellijalpine.attributes.AttributeInfo import com.intellij.psi.PsiElement import com.intellij.psi.meta.PsiPresentableMetaData import com.intellij.psi.xml.XmlTag diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt index 13ba8ec..da77b81 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeInfo.kt @@ -5,7 +5,7 @@ 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'", diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt index e9d76bb..44fd2c8 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/attributes/AttributeUtil.kt @@ -5,12 +5,8 @@ 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 object AttributeUtil { private val corePrefixes = listOf( diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt index 5ae2a39..a4b456d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpineAttributeCompletionProvider.kt @@ -1,7 +1,6 @@ package com.github.inxilpro.intellijalpine.completion import com.github.inxilpro.intellijalpine.Alpine -import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions import com.github.inxilpro.intellijalpine.support.LanguageUtil import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt index b5216d3..df9b1fe 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AlpinePluginCompletionProviderWrapper.kt @@ -10,7 +10,7 @@ import com.intellij.util.ProcessingContext abstract class AlpinePluginCompletionProvider( private val plugin: AlpinePlugin ) : CompletionProvider() { - + final override fun addCompletions( parameters: CompletionParameters, context: ProcessingContext, diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt index a7f8afa..dc42ced 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/AlpinePlugin.kt @@ -8,7 +8,8 @@ import org.apache.commons.lang3.tuple.MutablePair interface AlpinePlugin { companion object { - val EP_NAME = ExtensionPointName.Companion.create("com.github.inxilpro.intellijalpine.alpinePlugin") + val EP_NAME = + ExtensionPointName.Companion.create("com.github.inxilpro.intellijalpine.alpinePlugin") } fun getPluginName(): String @@ -24,7 +25,7 @@ interface AlpinePlugin { fun directiveSupportJavaScript(directive: String): Boolean = true fun injectAutoCompleteSuggestions(suggestions: AutoCompleteSuggestions) {} - + fun getCompletionProviders(): List = emptyList() fun getDirectives(): List = emptyList() 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 index 7e49476..5f0bc4f 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/PluginDetector.kt @@ -3,12 +3,12 @@ package com.github.inxilpro.intellijalpine.core.detection import com.github.inxilpro.intellijalpine.core.AlpinePlugin import com.intellij.openapi.project.Project -class PluginDetector: DetectionStrategy { +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) } 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 index 1daf1b3..f13410f 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/core/detection/ScriptReferenceDetector.kt @@ -56,7 +56,7 @@ class ScriptReferenceDetector : DetectionStrategy { for (extension in jsExtensions) { jsFiles.addAll( - FilenameIndex.getAllFilesByExt(project,extension,GlobalSearchScope.projectScope(project)) + FilenameIndex.getAllFilesByExt(project, extension, GlobalSearchScope.projectScope(project)) ) } diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt index 197d585..d7720ff 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt @@ -1,8 +1,8 @@ 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.github.inxilpro.intellijalpine.attributes.AttributeUtil import com.intellij.lang.Language import com.intellij.lang.injection.MultiHostInjector import com.intellij.lang.injection.MultiHostRegistrar @@ -150,7 +150,7 @@ class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { } private fun getPrefixAndSuffix(directive: String, host: XmlAttributeValue): Pair { - val globalContext = MutablePair(globalMagics, ""); + val globalContext = MutablePair(globalMagics, "") val context = AlpinePluginRegistry.instance.injectAllJsContext(host.project, globalContext) if ("x-data" != directive) { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt index 027bc64..062d454 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineAjaxPlugin.kt @@ -1,19 +1,10 @@ package com.github.inxilpro.intellijalpine.plugins -import com.github.inxilpro.intellijalpine.core.AlpinePlugin -import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration import com.github.inxilpro.intellijalpine.attributes.AttributeInfo import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.patterns.PlatformPatterns +import com.github.inxilpro.intellijalpine.core.AlpinePlugin +import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration import com.intellij.patterns.XmlPatterns -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 import com.intellij.psi.xml.XmlTokenType import org.apache.commons.lang3.tuple.MutablePair diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt index d6db3e9..514eccd 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineTargetReferenceContributor.kt @@ -4,7 +4,12 @@ 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.* +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 @@ -87,7 +92,7 @@ class AlpineIdReference( 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() diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt index 96443db..06d4b8e 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/AlpineWizardPlugin.kt @@ -3,14 +3,6 @@ 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.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 import org.apache.commons.lang3.tuple.MutablePair class AlpineWizardPlugin : AlpinePlugin { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt index b9f49f7..5cce162 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/plugins/TooltipPlugin.kt @@ -1,20 +1,8 @@ package com.github.inxilpro.intellijalpine.plugins -import com.github.inxilpro.intellijalpine.core.AlpinePlugin -import com.github.inxilpro.intellijalpine.core.CompletionProviderRegistration import com.github.inxilpro.intellijalpine.attributes.AttributeInfo import com.github.inxilpro.intellijalpine.completion.AutoCompleteSuggestions -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.patterns.PlatformPatterns -import com.intellij.patterns.XmlPatterns -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 -import com.intellij.psi.xml.XmlTokenType +import com.github.inxilpro.intellijalpine.core.AlpinePlugin import org.apache.commons.lang3.tuple.MutablePair class TooltipPlugin : AlpinePlugin { diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt index bed9a6e..ac3331d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineProjectSettingsState.kt @@ -6,7 +6,10 @@ 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")]) +@State( + name = "com.github.inxilpro.intellijalpine.AlpineProjectSettingsState", + storages = [Storage("alpinejs-support.xml")] +) class AlpineProjectSettingsState : PersistentStateComponent { var enabledPlugins = mutableMapOf() diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt index 7b8b8a4..c6447d9 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -42,13 +42,13 @@ class AlpineSettingsComponent(private val project: Project?) { 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") diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt index 9c4ec20..77985d0 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt @@ -1,8 +1,6 @@ package com.github.inxilpro.intellijalpine.settings import com.github.inxilpro.intellijalpine.core.AlpinePluginRegistry -import com.github.inxilpro.intellijalpine.settings.AlpineSettingsComponent -import com.github.inxilpro.intellijalpine.settings.AlpineSettingsState import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project import javax.swing.JComponent diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt index fdc85aa..55b2e38 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/LanguageUtil.kt @@ -20,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/support/XmlExtension.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt index 3e435a5..03fd7e6 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/support/XmlExtension.kt @@ -1,7 +1,6 @@ package com.github.inxilpro.intellijalpine.support import com.github.inxilpro.intellijalpine.attributes.AttributeUtil -import com.github.inxilpro.intellijalpine.support.LanguageUtil import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile import com.intellij.psi.html.HtmlTag diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index eefa1cb..de67665 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -21,22 +21,25 @@ - - + + - + implementationClass="com.github.inxilpro.intellijalpine.core.AlpineLineMarkerProvider"/> - + displayName="Alpine.js"/> 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 From f476fe32be9636e4b3c6df8793097b6f2395d232 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 20 Jun 2025 22:00:57 -0400 Subject: [PATCH 32/32] Fix qodana analysis --- .../github/inxilpro/intellijalpine/Alpine.kt | 2 - .../completion/AutoCompleteSuggestions.kt | 4 - .../AlpineJavaScriptAttributeValueInjector.kt | 135 +++++++++--------- .../settings/AlpineSettingsComponent.kt | 2 +- .../settings/AlpineSettingsConfigurable.kt | 1 + 5 files changed, 68 insertions(+), 76 deletions(-) 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/completion/AutoCompleteSuggestions.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt index a8c9eb5..43cb9be 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/completion/AutoCompleteSuggestions.kt @@ -102,10 +102,6 @@ class AutoCompleteSuggestions(val htmlTag: HtmlTag, val partialAttribute: String } } - fun addModifiers(modifiableDirective: String, modifiers: List) { - addModifiers(modifiableDirective, modifiers.toTypedArray()) - } - fun addModifiers(modifiableDirective: String, modifiers: Array) { if (!partialAttribute.startsWith(modifiableDirective)) { return diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt index d7720ff..bda505d 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/injection/AlpineJavaScriptAttributeValueInjector.kt @@ -19,74 +19,71 @@ import org.apache.commons.lang3.tuple.MutablePair import org.apache.html.dom.HTMLDocumentImpl class AlpineJavaScriptAttributeValueInjector : MultiHostInjector { - private companion object { - val globalState = - """ - /** @type {Object.} */ - let ${'$'}refs; - - /** @type {Object.} */ - let ${'$'}store; - - """.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) { @@ -135,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 diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt index c6447d9..8446fed 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsComponent.kt @@ -10,7 +10,7 @@ import com.intellij.util.ui.UIUtil import javax.swing.JComponent import javax.swing.JPanel -class AlpineSettingsComponent(private val project: Project?) { +class AlpineSettingsComponent(project: Project?) { val panel: JPanel private val myShowGutterIconsStatus = JBCheckBox("Show Alpine gutter icons") diff --git a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt index 77985d0..c93a64b 100644 --- a/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/inxilpro/intellijalpine/settings/AlpineSettingsConfigurable.kt @@ -8,6 +8,7 @@ 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" }