From 11cff94c35f7b28b984b1d9746ba50b073f9c634 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 4 Feb 2025 19:48:32 +0100 Subject: [PATCH] Find usages for automation Using an undocumented API, but what the hell, it works just fine. --- .../HassAutomationFindUsagesProvider.kt | 46 ++++++++++++++ .../HassScriptFindUsagesProvider.kt | 18 ++++-- .../hass/plugin/language/HassAutomation.kt | 41 +++++++++++++ .../plugin/language/HassElementEvaluator.kt | 60 +++++++++++++++++++ .../hass/plugin/psi/HassEntityReference.kt | 6 +- .../plugin/services/HassDataRepository.kt | 8 ++- .../kotlin/it/casaricci/hass/plugin/utils.kt | 15 +++++ src/main/resources/META-INF/plugin.xml | 6 ++ .../resources/messages/MyBundle.properties | 1 + 9 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassAutomationFindUsagesProvider.kt create mode 100644 src/main/kotlin/it/casaricci/hass/plugin/language/HassAutomation.kt create mode 100644 src/main/kotlin/it/casaricci/hass/plugin/language/HassElementEvaluator.kt diff --git a/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassAutomationFindUsagesProvider.kt b/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassAutomationFindUsagesProvider.kt new file mode 100644 index 0000000..9aec74d --- /dev/null +++ b/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassAutomationFindUsagesProvider.kt @@ -0,0 +1,46 @@ +package it.casaricci.hass.plugin.findUsages + +import com.intellij.lang.cacheBuilder.WordsScanner +import com.intellij.lang.findUsages.FindUsagesProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement +import it.casaricci.hass.plugin.MyBundle +import it.casaricci.hass.plugin.language.HassAutomation +import org.jetbrains.yaml.YAMLWordsScanner + +class HassAutomationFindUsagesProvider : FindUsagesProvider { + + override fun getWordsScanner(): WordsScanner { + return YAMLWordsScanner() + } + + override fun canFindUsagesFor(element: PsiElement): Boolean { + return isMyElement(element) + } + + override fun getHelpId(element: PsiElement): String? { + return null + } + + override fun getType(element: PsiElement): String { + if (isMyElement(element)) { + return MyBundle.message("hass.findUsages.haAutomation") + } + return "" + } + + override fun getDescriptiveName(element: PsiElement): String { + if (isMyElement(element)) { + return (element as PsiNamedElement).name ?: "" + } + return "" + } + + override fun getNodeText(element: PsiElement, useFullName: Boolean): String { + return getDescriptiveName(element) + } + + private fun isMyElement(element: PsiElement): Boolean { + return element is HassAutomation + } +} diff --git a/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassScriptFindUsagesProvider.kt b/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassScriptFindUsagesProvider.kt index 315df7b..ee1b3cf 100644 --- a/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassScriptFindUsagesProvider.kt +++ b/src/main/kotlin/it/casaricci/hass/plugin/findUsages/HassScriptFindUsagesProvider.kt @@ -3,11 +3,9 @@ package it.casaricci.hass.plugin.findUsages import com.intellij.lang.cacheBuilder.WordsScanner import com.intellij.lang.findUsages.FindUsagesProvider import com.intellij.psi.PsiElement -import it.casaricci.hass.plugin.HassKnownDomains import it.casaricci.hass.plugin.MyBundle import it.casaricci.hass.plugin.isScriptDefinition import org.jetbrains.yaml.YAMLWordsScanner -import org.jetbrains.yaml.psi.YAMLKeyValue class HassScriptFindUsagesProvider : FindUsagesProvider { @@ -30,17 +28,25 @@ class HassScriptFindUsagesProvider : FindUsagesProvider { return "" } + /** + * Called but return value is not used probably because YAML plugin takes precedence. + */ override fun getDescriptiveName(element: PsiElement): String { - if (isMyElement(element)) { + // since this is not used (for now), avoid losing time doing useless stuff + /*if (isMyElement(element)) { return element.text - } + }*/ return "" } + /** + * Actually never called because YAML plugin takes precedence. + */ override fun getNodeText(element: PsiElement, useFullName: Boolean): String { - if (isMyElement(element)) { + // since this is not used (for now), avoid losing time doing useless stuff + /*if (isMyElement(element)) { return element.text + ":" - } + }*/ return "" } diff --git a/src/main/kotlin/it/casaricci/hass/plugin/language/HassAutomation.kt b/src/main/kotlin/it/casaricci/hass/plugin/language/HassAutomation.kt new file mode 100644 index 0000000..9adb68a --- /dev/null +++ b/src/main/kotlin/it/casaricci/hass/plugin/language/HassAutomation.kt @@ -0,0 +1,41 @@ +package it.casaricci.hass.plugin.language + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.IncorrectOperationException +import org.jetbrains.yaml.YAMLBundle +import org.jetbrains.yaml.YAMLElementGenerator +import org.jetbrains.yaml.psi.YAMLScalar +import org.jetbrains.yaml.psi.impl.YAMLKeyValueImpl + +/** + * Wraps the "alias" key value element of an automation, giving the element [PsiNamedElement] features. + */ +class HassAutomation(private val element: YAMLScalar) : PsiNamedElement, YAMLScalar by element { + + /** + * Automation name is actually the value of the "alias" key. + */ + override fun getName(): String { + return element.textValue + } + + /** + * Copied from [YAMLKeyValueImpl] + [org.jetbrains.yaml.YAMLUtil], although it seems like overkill. + */ + override fun setName(name: String): PsiElement { + if (name == element.textValue) { + throw IncorrectOperationException(YAMLBundle.message("rename.same.name")) + } + + val elementGenerator = YAMLElementGenerator.getInstance(element.project) + + val tempFile: PsiFile = elementGenerator.createDummyYamlWithText(name) + val textElement = PsiTreeUtil.collectElementsOfType(tempFile, YAMLScalar::class.java).iterator().next() + + element.replace(textElement) + return this + } +} diff --git a/src/main/kotlin/it/casaricci/hass/plugin/language/HassElementEvaluator.kt b/src/main/kotlin/it/casaricci/hass/plugin/language/HassElementEvaluator.kt new file mode 100644 index 0000000..fc4717e --- /dev/null +++ b/src/main/kotlin/it/casaricci/hass/plugin/language/HassElementEvaluator.kt @@ -0,0 +1,60 @@ +package it.casaricci.hass.plugin.language + +import com.intellij.codeInsight.TargetElementEvaluatorEx2 +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.parentOfType +import it.casaricci.hass.plugin.isAutomation +import org.jetbrains.yaml.psi.YAMLScalar + +class HassElementEvaluator : TargetElementEvaluatorEx2() { + + /** + * This method will be called for elements that don't inherit from [com.intellij.psi.PsiNamedElement]. + * @return an actual [com.intellij.psi.PsiNamedElement] that can be named and used as a reference + */ + override fun getNamedElement(element: PsiElement): PsiElement? { + // handle text values (e.g. property values) + val textElement = element.parentOfType() + if (textElement != null) { + return wrapElement(textElement) + } + + return null + } + + /** + * This method will be called for practically every element selected by the user, so it needs to be very fast. + * @return an element wrapped with a custom class that we can recognize (if available, otherwise the same element + * will be returned) + */ + override fun adjustReferenceOrReferencedElement( + file: PsiFile, + editor: Editor, + offset: Int, + flags: Int, + refElement: PsiElement? + ): PsiElement? { + val wrappedElement = when (refElement) { + is YAMLScalar -> { + wrapElement(refElement) + } + + else -> { + null + } + } + + return wrappedElement ?: super.adjustReferenceOrReferencedElement(file, editor, offset, flags, refElement) + } + + private fun wrapElement(element: YAMLScalar): PsiNamedElement? { + if (isAutomation(element)) { + return HassAutomation(element) + } + return null + } + +} diff --git a/src/main/kotlin/it/casaricci/hass/plugin/psi/HassEntityReference.kt b/src/main/kotlin/it/casaricci/hass/plugin/psi/HassEntityReference.kt index 8adc40b..94c6d2c 100644 --- a/src/main/kotlin/it/casaricci/hass/plugin/psi/HassEntityReference.kt +++ b/src/main/kotlin/it/casaricci/hass/plugin/psi/HassEntityReference.kt @@ -77,10 +77,14 @@ class HassEntityReference( .toTypedArray() } + /** + * Automations are identified by the value of their "alias" key. The actual PSI element is wrapped by + * [it.casaricci.hass.plugin.language.HassAutomation]. + */ private fun handleAutomation(module: Module, entityName: String): Array { val service = HassDataRepository.getInstance(module.project) return service.getAutomations(module).filter { - it.valueText == entityName + it.textValue == entityName } .map { result -> PsiElementResolveResult(result) } .toTypedArray() diff --git a/src/main/kotlin/it/casaricci/hass/plugin/services/HassDataRepository.kt b/src/main/kotlin/it/casaricci/hass/plugin/services/HassDataRepository.kt index e5315d4..6c8d81f 100644 --- a/src/main/kotlin/it/casaricci/hass/plugin/services/HassDataRepository.kt +++ b/src/main/kotlin/it/casaricci/hass/plugin/services/HassDataRepository.kt @@ -23,9 +23,10 @@ import org.jetbrains.yaml.YAMLUtil import org.jetbrains.yaml.psi.YAMLFile import org.jetbrains.yaml.psi.YAMLKeyValue import org.jetbrains.yaml.psi.YAMLMapping +import org.jetbrains.yaml.psi.YAMLScalar import org.jetbrains.yaml.psi.YAMLSequence -private val AUTOMATIONS_CACHE = Key>>("HASS_AUTOMATIONS_CACHE") +private val AUTOMATIONS_CACHE = Key>>("HASS_AUTOMATIONS_CACHE") private val ACTIONS_CACHE = Key>>("HASS_ACTIONS_CACHE") private val ENTITIES_CACHE = Key>>("HASS_ENTITIES_CACHE") private val SECRETS_CACHE = Key>>("HASS_SECRETS_CACHE") @@ -142,7 +143,7 @@ class HassDataRepository(private val project: Project) { /** * List of all automations. Uses local data only. */ - fun getAutomations(module: Module): Collection { + fun getAutomations(module: Module): Collection { return CachedValuesManager.getManager(project).getCachedValue( module, AUTOMATIONS_CACHE, @@ -150,6 +151,7 @@ class HassDataRepository(private val project: Project) { CachedValueProvider.Result.create(buildList { for (yamlFile in findAllYamlPsiFiles(module)) { // TODO is there a more efficient way to do this? This seems like an overkill... + // TODO this logic should be centralized somewhere maybe? YAMLUtil.getQualifiedKeyInFile(yamlFile, HassKnownDomains.AUTOMATION)?.let { automationBlock -> automationBlock.childrenOfType().firstOrNull()?.let { automations -> addAll( @@ -158,7 +160,7 @@ class HassDataRepository(private val project: Project) { automation.childrenOfType().first().keyValues .firstOrNull { automationProperty -> automationProperty.keyText == HASS_AUTOMATION_NAME_PROPERTY - } + }?.value as? YAMLScalar }) } } diff --git a/src/main/kotlin/it/casaricci/hass/plugin/utils.kt b/src/main/kotlin/it/casaricci/hass/plugin/utils.kt index 48d410c..54ca9e4 100644 --- a/src/main/kotlin/it/casaricci/hass/plugin/utils.kt +++ b/src/main/kotlin/it/casaricci/hass/plugin/utils.kt @@ -7,11 +7,14 @@ import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.text.StringUtilRt import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType import com.intellij.util.io.CountingGZIPInputStream import org.apache.commons.io.input.CountingInputStream import org.jetbrains.yaml.YAMLFileType import org.jetbrains.yaml.psi.YAMLKeyValue import org.jetbrains.yaml.psi.YAMLScalar +import org.jetbrains.yaml.psi.YAMLSequence +import org.jetbrains.yaml.psi.YAMLSequenceItem import java.io.InputStream // TODO this should start from configuration.yaml and walk all includes (in order to filter out unwanted files) @@ -55,6 +58,18 @@ fun isScriptDefinition(element: PsiElement): Boolean { (element.parentMapping?.parent as YAMLKeyValue).keyText == HassKnownDomains.SCRIPT } +fun isAutomation(element: PsiElement?): Boolean { + if (element is YAMLScalar) { + val keyValue = element.parentOfType() + return (keyValue?.keyText == HASS_AUTOMATION_NAME_PROPERTY && + keyValue.parentOfType() + ?.parentOfType() + ?.parentOfType()?.keyText == HassKnownDomains.AUTOMATION + ) + } + return false +} + /** * An [InputStream] that updates a [ProgressIndicator] while being read. * To be used with [com.intellij.util.io.HttpRequests], handles also [CountingGZIPInputStream] for compressed streams. diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6bc3b96..6b20002 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -14,12 +14,18 @@ + + + diff --git a/src/main/resources/messages/MyBundle.properties b/src/main/resources/messages/MyBundle.properties index a0c3f12..916684e 100644 --- a/src/main/resources/messages/MyBundle.properties +++ b/src/main/resources/messages/MyBundle.properties @@ -9,6 +9,7 @@ hass.facet.editor.token.invalid=A token is needed to download data from Home Ass hass.facet.editor.refresh.text=Refresh data hass.findUsages.haScript=Home Assistant script +hass.findUsages.haAutomation=Home Assistant automation hass.findUsages.haSecret=Home Assistant secret hass.notification.refreshCache.progress=Updating states data from Home Assistant