From 9cba6c2f8532d1e6b875d652e97063b5f34f4aac Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sat, 8 Mar 2025 11:23:54 +0200 Subject: [PATCH 1/3] Go to commands defined in mixin files --- .../intellijlets/LetsReferenceContributor.kt | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt index 34cd7aa..181934c 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,41 @@ 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 + return findCommandInMixins(yamlFile, commandName) + } + /** + * Searches for the given command name in mixin files. + */ + private fun findCommandInMixins(yamlFile: YAMLFile, commandName: String): PsiElement? { + // Find the mixins key in the YAML file + val mixinsKey = PsiTreeUtil.findChildrenOfType(yamlFile, 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 + } + } + + return null } } @@ -81,7 +115,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 From d0ad9f8735b25135d43b623058c35ea40fac9a81 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sat, 8 Mar 2025 11:40:41 +0200 Subject: [PATCH 2/3] Go to definition of command in mixin file when in another mixin file --- README.md | 5 ++- .../intellijlets/LetsReferenceContributor.kt | 34 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) 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 181934c..3590e2c 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt @@ -48,15 +48,22 @@ class LetsDependsReference(element: YAMLScalar) : PsiReferenceBase(e return localCommand } - // Search for the command in mixin files - return findCommandInMixins(yamlFile, commandName) + // Search for the command in mixin files (with recursive support) + return findCommandInMixins(yamlFile, commandName, mutableSetOf()) } /** - * Searches for the given command name in mixin files. + * Recursively searches for the given command name in mixin files. */ - private fun findCommandInMixins(yamlFile: YAMLFile, commandName: String): PsiElement? { - // Find the mixins key in the YAML file - val mixinsKey = PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java) + 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 @@ -73,6 +80,12 @@ class LetsDependsReference(element: YAMLScalar) : PsiReferenceBase(e if (command != null) { return command } + + // Recursively check mixins within this mixin + val nestedCommand = findCommandInMixins(mixinFile, commandName, visitedFiles) + if (nestedCommand != null) { + return nestedCommand + } } return null @@ -123,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 From 702a2f45348a5fc9d92c9b18654fb1d31817a6ed Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sat, 8 Mar 2025 11:49:05 +0200 Subject: [PATCH 3/3] add test for command reference in mixin file --- .../intellijlets/reference/ReferenceTest.kt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) 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",