Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -40,8 +41,54 @@ class LetsDependsReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(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<YAMLFile>): 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
}
}

Expand Down Expand Up @@ -81,12 +128,21 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(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
return FilenameIndex.getVirtualFilesByName(
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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: [<caret>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: [<caret>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",
Expand Down