Skip to content

Commit

Permalink
Find usages for automation
Browse files Browse the repository at this point in the history
Using an undocumented API, but what the hell, it works just fine.
  • Loading branch information
daniele-athome authored Feb 4, 2025
1 parent c22809f commit 11cff94
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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 ""
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<YAMLScalar>()
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
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResolveResult> {
val service = HassDataRepository.getInstance(module.project)
return service.getAutomations(module).filter {
it.valueText == entityName
it.textValue == entityName
}
.map { result -> PsiElementResolveResult(result) }
.toTypedArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CachedValue<Collection<YAMLKeyValue>>>("HASS_AUTOMATIONS_CACHE")
private val AUTOMATIONS_CACHE = Key<CachedValue<Collection<YAMLScalar>>>("HASS_AUTOMATIONS_CACHE")
private val ACTIONS_CACHE = Key<CachedValue<Collection<PsiNamedElement>>>("HASS_ACTIONS_CACHE")
private val ENTITIES_CACHE = Key<CachedValue<Collection<PsiElement>>>("HASS_ENTITIES_CACHE")
private val SECRETS_CACHE = Key<CachedValue<Collection<YAMLKeyValue>>>("HASS_SECRETS_CACHE")
Expand Down Expand Up @@ -142,14 +143,15 @@ class HassDataRepository(private val project: Project) {
/**
* List of all automations. Uses local data only.
*/
fun getAutomations(module: Module): Collection<YAMLKeyValue> {
fun getAutomations(module: Module): Collection<YAMLScalar> {
return CachedValuesManager.getManager(project).getCachedValue(
module,
AUTOMATIONS_CACHE,
{
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<YAMLSequence>().firstOrNull()?.let { automations ->
addAll(
Expand All @@ -158,7 +160,7 @@ class HassDataRepository(private val project: Project) {
automation.childrenOfType<YAMLMapping>().first().keyValues
.firstOrNull { automationProperty ->
automationProperty.keyText == HASS_AUTOMATION_NAME_PROPERTY
}
}?.value as? YAMLScalar
})
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/it/casaricci/hass/plugin/utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<YAMLKeyValue>()
return (keyValue?.keyText == HASS_AUTOMATION_NAME_PROPERTY &&
keyValue.parentOfType<YAMLSequenceItem>()
?.parentOfType<YAMLSequence>()
?.parentOfType<YAMLKeyValue>()?.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.
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@
<psi.referenceContributor language="yaml"
implementation="it.casaricci.hass.plugin.psi.HassReferenceContributor"/>

<targetElementEvaluator language="yaml"
implementationClass="it.casaricci.hass.plugin.language.HassElementEvaluator"/>

<completion.contributor language="yaml"
implementationClass="it.casaricci.hass.plugin.completion.HassCompletionContributor"/>

<lang.findUsagesProvider language="yaml"
implementationClass="it.casaricci.hass.plugin.findUsages.HassScriptFindUsagesProvider"
order="before yamlFindUsagesProvider"/>
<lang.findUsagesProvider language="yaml"
implementationClass="it.casaricci.hass.plugin.findUsages.HassAutomationFindUsagesProvider"
order="before yamlFindUsagesProvider"/>
<lang.findUsagesProvider language="yaml"
implementationClass="it.casaricci.hass.plugin.findUsages.HassSecretFindUsagesProvider"
order="before yamlFindUsagesProvider"/>
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/messages/MyBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 11cff94

Please sign in to comment.