diff --git a/README.md b/README.md index ddd602b..4523685 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,15 @@ File type recognition for `lets.yaml` and `lets.*.yaml` configs - [ ] Complete env mode in `env` with code snippet - [x] Complete `LETS*` environment variables in cmd scripts - [ ] Complete environment variables for checksum + - [ ] Complete environment variables from global and command `env` in cmd scripts + - [ ] Complete environment variables in `args` - **Go To Definition** - [x] Navigate to definitions of `mixins` files - [x] Navigate to definitions of optional `mixins` files (with - at the beginning) - [x] Navigate to definitions of `mixins` remote files (as http links) - [x] Navigate to definitions of commands in `depends` - - [ ] Navigate to definitions of commands in `depends` from mixins + - [x] Navigate to definitions of commands in `depends` from mixins + - [ ] Navigate to definitions of commands in `ref` - [ ] Navigate to files in `checksum` - **Highlighting** - [x] Highlighting for shell script in `cmd` diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt index 34cd7aa..3590e2c 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt @@ -14,6 +14,7 @@ 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.YAMLSequenceItem open class LetsReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { @@ -40,8 +41,54 @@ class LetsDependsReference(element: YAMLScalar) : PsiReferenceBase(e // Locate the command declaration in the same YAML file val yamlFile = myElement.containingFile as? YAMLFile ?: return null - return PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java) + val localCommand = PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java) .firstOrNull { it.keyText == commandName && it.parent is YAMLMapping } + + if (localCommand != null) { + return localCommand + } + + // Search for the command in mixin files (with recursive support) + return findCommandInMixins(yamlFile, commandName, mutableSetOf()) + } + /** + * Recursively searches for the given command name in mixin files. + */ + private fun findCommandInMixins(yamlFile: YAMLFile, commandName: String, visitedFiles: MutableSet): PsiElement? { + if (!visitedFiles.add(yamlFile)) { + return null // Prevent infinite recursion + } + + // If the current file is a mixin, retrieve the main lets.yaml file + val mainConfigFile = findMainConfigFile(yamlFile) ?: return null + + // Find the mixins key in the main lets.yaml file + val mixinsKey = PsiTreeUtil.findChildrenOfType(mainConfigFile, YAMLKeyValue::class.java) + .firstOrNull { it.keyText == "mixins" } ?: return null + + // Extract mixin file names from YAMLSequenceItems + val mixinFiles = mixinsKey.value?.children + ?.mapNotNull { it as? YAMLSequenceItem } + ?.mapNotNull { it.value as? YAMLScalar } + ?.mapNotNull { LetsMixinReference(it).resolve() as? YAMLFile } ?: return null + + // Search for the command in the resolved mixin files + for (mixinFile in mixinFiles) { + val command = PsiTreeUtil.findChildrenOfType(mixinFile, YAMLKeyValue::class.java) + .firstOrNull { it.keyText == commandName && it.parent is YAMLMapping } + + if (command != null) { + return command + } + + // Recursively check mixins within this mixin + val nestedCommand = findCommandInMixins(mixinFile, commandName, visitedFiles) + if (nestedCommand != null) { + return nestedCommand + } + } + + return null } } @@ -81,7 +128,7 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase(ele private fun findMixinFile(project: Project, mixinPath: String): VirtualFile? { // Normalize paths (handle both "lets.mixin.yaml" and "lets/lets.mixin.yaml") val normalizedPath = mixinPath.trimStart('/') - // Normalize gitignored files (e.g. "-lets.mixin.yaml" -> "lets.mixin.yaml") + // Normalize git-ignored files (e.g. "-lets.mixin.yaml" -> "lets.mixin.yaml") .removePrefix("-") // Look for an exact match in the project @@ -89,4 +136,13 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase(ele PathUtil.getFileName(normalizedPath), GlobalSearchScope.allScope(project) ).firstOrNull { it.path.endsWith(normalizedPath) } } +} + +/** + * Finds the main lets.yaml configuration file, assuming it is located at the root. + */ +private fun findMainConfigFile(currentFile: YAMLFile): YAMLFile? { + val project = currentFile.project + val mainFiles = FilenameIndex.getVirtualFilesByName("lets.yaml", GlobalSearchScope.projectScope(project)) + return mainFiles.mapNotNull { PsiManager.getInstance(project).findFile(it) as? YAMLFile }.firstOrNull() } \ No newline at end of file diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/reference/ReferenceTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/reference/ReferenceTest.kt index 6f6ef99..082f23e 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/reference/ReferenceTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/reference/ReferenceTest.kt @@ -37,6 +37,99 @@ open class MinixsReferenceTest : BasePlatformTestCase() { assertEquals("lets.mixin.yaml", resolvedFile.name) } + fun testDependsCommandInMixinReference() { + myFixture.addFileToProject( + "mixins/lets.mixin.yaml", + """ + shell: bash + + commands: + test: + cmd: echo Test + """.trimIndent() + ) + + myFixture.configureByText( + "lets.yaml", + """ + shell: bash + mixins: + - mixins/lets.mixin.yaml + + commands: + run: + depends: [test] + cmd: echo Run + """.trimIndent() + ) + + val ref = myFixture.getReferenceAtCaretPosition("lets.yaml") + assertNotNull("Reference should not be null", ref) + + val resolvedElement = ref!!.resolve() + assertNotNull("Resolved element should not be null", resolvedElement) + + val resolvedFile = resolvedElement?.containingFile + assertEquals("lets.mixin.yaml", resolvedFile?.name) + + val resolvedKey = resolvedElement as? YAMLKeyValue + assertNotNull("Resolved element should be a YAMLKeyValue", resolvedKey) + assertEquals("test", resolvedKey!!.keyText) + } + + fun testDependsCommandCrossMixinReference() { + myFixture.addFileToProject( + "mixins/lets.build.yaml", + """ + shell: bash + + commands: + build: + cmd: echo Build + """.trimIndent() + ) + + myFixture.addFileToProject( + "mixins/lets.deploy.yaml", + """ + shell: bash + + commands: + deploy: + depends: [build] + cmd: echo Deploy + """.trimIndent() + ) + + myFixture.configureByText( + "lets.yaml", + """ + shell: bash + mixins: + - mixins/lets.build.yaml + - mixins/lets.deploy.yaml + + commands: + run: + depends: [deploy] + cmd: echo Run + """.trimIndent() + ) + + val ref = myFixture.getReferenceAtCaretPosition("mixins/lets.deploy.yaml") + assertNotNull("Reference should not be null", ref) + + val resolvedElement = ref!!.resolve() + assertNotNull("Resolved element should not be null", resolvedElement) + + val resolvedFile = resolvedElement?.containingFile + assertEquals("lets.build.yaml", resolvedFile?.name) + + val resolvedKey = resolvedElement as? YAMLKeyValue + assertNotNull("Resolved element should be a YAMLKeyValue", resolvedKey) + assertEquals("build", resolvedKey!!.keyText) + } + fun testMixinFileInDirReference() { myFixture.addFileToProject( "mixins/lets.mixin.yaml",