Skip to content

Commit f315ebf

Browse files
authored
Merge pull request #45 from lets-cli/go-to-commands-defined-in-mixins
Go to commands defined in mixin files
2 parents 30c8922 + 702a2f4 commit f315ebf

File tree

3 files changed

+155
-3
lines changed

3 files changed

+155
-3
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ File type recognition for `lets.yaml` and `lets.*.yaml` configs
1818
- [ ] Complete env mode in `env` with code snippet
1919
- [x] Complete `LETS*` environment variables in cmd scripts
2020
- [ ] Complete environment variables for checksum
21+
- [ ] Complete environment variables from global and command `env` in cmd scripts
22+
- [ ] Complete environment variables in `args`
2123
- **Go To Definition**
2224
- [x] Navigate to definitions of `mixins` files
2325
- [x] Navigate to definitions of optional `mixins` files (with - at the beginning)
2426
- [x] Navigate to definitions of `mixins` remote files (as http links)
2527
- [x] Navigate to definitions of commands in `depends`
26-
- [ ] Navigate to definitions of commands in `depends` from mixins
28+
- [x] Navigate to definitions of commands in `depends` from mixins
29+
- [ ] Navigate to definitions of commands in `ref`
2730
- [ ] Navigate to files in `checksum`
2831
- **Highlighting**
2932
- [x] Highlighting for shell script in `cmd`

src/main/kotlin/com/github/kindermax/intellijlets/LetsReferenceContributor.kt

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.jetbrains.yaml.psi.YAMLFile
1414
import org.jetbrains.yaml.psi.YAMLKeyValue
1515
import org.jetbrains.yaml.psi.YAMLMapping
1616
import org.jetbrains.yaml.psi.YAMLScalar
17+
import org.jetbrains.yaml.psi.YAMLSequenceItem
1718

1819
open class LetsReferenceContributor : PsiReferenceContributor() {
1920
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
@@ -40,8 +41,54 @@ class LetsDependsReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(e
4041

4142
// Locate the command declaration in the same YAML file
4243
val yamlFile = myElement.containingFile as? YAMLFile ?: return null
43-
return PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java)
44+
val localCommand = PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java)
4445
.firstOrNull { it.keyText == commandName && it.parent is YAMLMapping }
46+
47+
if (localCommand != null) {
48+
return localCommand
49+
}
50+
51+
// Search for the command in mixin files (with recursive support)
52+
return findCommandInMixins(yamlFile, commandName, mutableSetOf())
53+
}
54+
/**
55+
* Recursively searches for the given command name in mixin files.
56+
*/
57+
private fun findCommandInMixins(yamlFile: YAMLFile, commandName: String, visitedFiles: MutableSet<YAMLFile>): PsiElement? {
58+
if (!visitedFiles.add(yamlFile)) {
59+
return null // Prevent infinite recursion
60+
}
61+
62+
// If the current file is a mixin, retrieve the main lets.yaml file
63+
val mainConfigFile = findMainConfigFile(yamlFile) ?: return null
64+
65+
// Find the mixins key in the main lets.yaml file
66+
val mixinsKey = PsiTreeUtil.findChildrenOfType(mainConfigFile, YAMLKeyValue::class.java)
67+
.firstOrNull { it.keyText == "mixins" } ?: return null
68+
69+
// Extract mixin file names from YAMLSequenceItems
70+
val mixinFiles = mixinsKey.value?.children
71+
?.mapNotNull { it as? YAMLSequenceItem }
72+
?.mapNotNull { it.value as? YAMLScalar }
73+
?.mapNotNull { LetsMixinReference(it).resolve() as? YAMLFile } ?: return null
74+
75+
// Search for the command in the resolved mixin files
76+
for (mixinFile in mixinFiles) {
77+
val command = PsiTreeUtil.findChildrenOfType(mixinFile, YAMLKeyValue::class.java)
78+
.firstOrNull { it.keyText == commandName && it.parent is YAMLMapping }
79+
80+
if (command != null) {
81+
return command
82+
}
83+
84+
// Recursively check mixins within this mixin
85+
val nestedCommand = findCommandInMixins(mixinFile, commandName, visitedFiles)
86+
if (nestedCommand != null) {
87+
return nestedCommand
88+
}
89+
}
90+
91+
return null
4592
}
4693
}
4794

@@ -81,12 +128,21 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(ele
81128
private fun findMixinFile(project: Project, mixinPath: String): VirtualFile? {
82129
// Normalize paths (handle both "lets.mixin.yaml" and "lets/lets.mixin.yaml")
83130
val normalizedPath = mixinPath.trimStart('/')
84-
// Normalize gitignored files (e.g. "-lets.mixin.yaml" -> "lets.mixin.yaml")
131+
// Normalize git-ignored files (e.g. "-lets.mixin.yaml" -> "lets.mixin.yaml")
85132
.removePrefix("-")
86133

87134
// Look for an exact match in the project
88135
return FilenameIndex.getVirtualFilesByName(
89136
PathUtil.getFileName(normalizedPath), GlobalSearchScope.allScope(project)
90137
).firstOrNull { it.path.endsWith(normalizedPath) }
91138
}
139+
}
140+
141+
/**
142+
* Finds the main lets.yaml configuration file, assuming it is located at the root.
143+
*/
144+
private fun findMainConfigFile(currentFile: YAMLFile): YAMLFile? {
145+
val project = currentFile.project
146+
val mainFiles = FilenameIndex.getVirtualFilesByName("lets.yaml", GlobalSearchScope.projectScope(project))
147+
return mainFiles.mapNotNull { PsiManager.getInstance(project).findFile(it) as? YAMLFile }.firstOrNull()
92148
}

src/test/kotlin/com/github/kindermax/intellijlets/reference/ReferenceTest.kt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,99 @@ open class MinixsReferenceTest : BasePlatformTestCase() {
3737
assertEquals("lets.mixin.yaml", resolvedFile.name)
3838
}
3939

40+
fun testDependsCommandInMixinReference() {
41+
myFixture.addFileToProject(
42+
"mixins/lets.mixin.yaml",
43+
"""
44+
shell: bash
45+
46+
commands:
47+
test:
48+
cmd: echo Test
49+
""".trimIndent()
50+
)
51+
52+
myFixture.configureByText(
53+
"lets.yaml",
54+
"""
55+
shell: bash
56+
mixins:
57+
- mixins/lets.mixin.yaml
58+
59+
commands:
60+
run:
61+
depends: [<caret>test]
62+
cmd: echo Run
63+
""".trimIndent()
64+
)
65+
66+
val ref = myFixture.getReferenceAtCaretPosition("lets.yaml")
67+
assertNotNull("Reference should not be null", ref)
68+
69+
val resolvedElement = ref!!.resolve()
70+
assertNotNull("Resolved element should not be null", resolvedElement)
71+
72+
val resolvedFile = resolvedElement?.containingFile
73+
assertEquals("lets.mixin.yaml", resolvedFile?.name)
74+
75+
val resolvedKey = resolvedElement as? YAMLKeyValue
76+
assertNotNull("Resolved element should be a YAMLKeyValue", resolvedKey)
77+
assertEquals("test", resolvedKey!!.keyText)
78+
}
79+
80+
fun testDependsCommandCrossMixinReference() {
81+
myFixture.addFileToProject(
82+
"mixins/lets.build.yaml",
83+
"""
84+
shell: bash
85+
86+
commands:
87+
build:
88+
cmd: echo Build
89+
""".trimIndent()
90+
)
91+
92+
myFixture.addFileToProject(
93+
"mixins/lets.deploy.yaml",
94+
"""
95+
shell: bash
96+
97+
commands:
98+
deploy:
99+
depends: [<caret>build]
100+
cmd: echo Deploy
101+
""".trimIndent()
102+
)
103+
104+
myFixture.configureByText(
105+
"lets.yaml",
106+
"""
107+
shell: bash
108+
mixins:
109+
- mixins/lets.build.yaml
110+
- mixins/lets.deploy.yaml
111+
112+
commands:
113+
run:
114+
depends: [deploy]
115+
cmd: echo Run
116+
""".trimIndent()
117+
)
118+
119+
val ref = myFixture.getReferenceAtCaretPosition("mixins/lets.deploy.yaml")
120+
assertNotNull("Reference should not be null", ref)
121+
122+
val resolvedElement = ref!!.resolve()
123+
assertNotNull("Resolved element should not be null", resolvedElement)
124+
125+
val resolvedFile = resolvedElement?.containingFile
126+
assertEquals("lets.build.yaml", resolvedFile?.name)
127+
128+
val resolvedKey = resolvedElement as? YAMLKeyValue
129+
assertNotNull("Resolved element should be a YAMLKeyValue", resolvedKey)
130+
assertEquals("build", resolvedKey!!.keyText)
131+
}
132+
40133
fun testMixinFileInDirReference() {
41134
myFixture.addFileToProject(
42135
"mixins/lets.mixin.yaml",

0 commit comments

Comments
 (0)