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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
# intellij-lets Changelog

## [Unreleased]
### Updated

- make go to definition work for gitignore `mixins` files (with '-' (dash) at the beginning)
- support global `init` in config parser and completion
- support `sh` and `checksum` modes in `env` in config parser
- support `ref` and `args` in command keyword completion
- support `mixins` (both local and remote) in config parser
- drop `eval_env`

### Chore

- add tests for reference contributor

## [0.0.16] - 2025-03-07

Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ File type recognition for `lets.yaml` and `lets.*.yaml` configs

- **Completion**
- Complete keywords
- Complete command `options` with code snippet
- Complete commands in `depends` with code snippet
- [x] Complete command `options` with code snippet
- [x] Complete commands in `depends` with code snippet
- [ ] Complete commands in `depends` from mixins
- [ ] Complete env mode in `env` with code snippet
- **Go To Definition**
- Navigate to definitions of `mixins` files
- [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)
- [ ] Navigate to definitions of commands in `depends`
- [ ] Navigate to definitions of commands in `depends` from mixins
- **Highlighting**
- [ ] Highlighting for shell script in cmd

<!-- Plugin description end -->

Expand Down Expand Up @@ -100,6 +108,4 @@ See https://detekt.github.io/detekt/ Rule Sets to get more info about `detekt` f

## Todo

- add highlighting for shell script in cmd - https://plugins.jetbrains.com/docs/intellij/file-view-providers.html
- read mixins - https://plugins.jetbrains.com/docs/intellij/psi-cookbook.html#how-do-i-find-a-file-if-i-know-its-name-but-don-t-know-the-path
- insert not padded strings, but yaml elements
111 changes: 90 additions & 21 deletions src/main/kotlin/com/github/kindermax/intellijlets/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@ import org.jetbrains.yaml.psi.YAMLMapping
import org.jetbrains.yaml.psi.YAMLScalar
import org.jetbrains.yaml.psi.YAMLSequence

typealias Env = Map<String, String>
sealed class EnvValue {
data class StringValue(val value: String) : EnvValue()
data class ShMode(val sh: String) : EnvValue()
data class ChecksumMode(val files: List<String>) : EnvValue()
data class ChecksumMapMode(val files: Map<String, List<String>>) : EnvValue()
}

typealias Env = Map<String, EnvValue>

sealed class Mixin {
data class Local(val path: String) : Mixin()
data class Remote(val url: String, val version: String) : Mixin()
}

typealias Mixins = List<Mixin>

data class Command(
val name: String,
val cmd: String,
val cmdAsMap: Map<String, String>,
val env: Env,
val evalEnv: Env,
val depends: List<String>,
)

Expand All @@ -34,9 +47,11 @@ class Config(
val commands: List<Command>,
val commandsMap: Map<String, Command>,
val env: Env,
val evalEnv: Env,
val before: String,
val specifiedDirectives: Set<String>,
val init: String,
val mixins: Mixins,
// Keywords that are used in the config
val keywordsInConfig: Set<String>,
) {

companion object Parser {
Expand All @@ -59,17 +74,49 @@ class Config(
emptyList(),
emptyMap(),
emptyMap(),
emptyMap(),
"",
"",
emptyList(),
emptySet(),
)
}

private fun parseEnv(keyValue: YAMLKeyValue): Env {
return when (val value = keyValue.value) {
is YAMLMapping -> value.keyValues.associate { kv -> kv.keyText to kv.valueText }
else -> emptyMap()
val value = keyValue.value as? YAMLMapping ?: return emptyMap()

return value.keyValues.associate { kv ->
kv.keyText to parseEnvValue(kv)
}
}

private fun parseEnvValue(kv: YAMLKeyValue): EnvValue {
return when (val envValue = kv.value) {
is YAMLScalar -> EnvValue.StringValue(envValue.textValue)
is YAMLMapping -> parseMappingEnvValue(envValue)
else -> EnvValue.StringValue("")
}
}

private fun parseMappingEnvValue(value: YAMLMapping): EnvValue {
value.keyValues.forEach { kv ->
when (kv.keyText) {
"sh" -> return EnvValue.ShMode(kv.valueText)
"checksum" -> {
return when (val checksumValue = kv.value) {
is YAMLSequence -> EnvValue.ChecksumMode(
checksumValue.items.mapNotNull { it.value?.text }
)
is YAMLMapping -> EnvValue.ChecksumMapMode(
checksumValue.keyValues.associate { entry ->
entry.keyText to (entry.value as YAMLSequence).items.mapNotNull { it.value?.text }
}
)
else -> EnvValue.StringValue("")
}
}
}
}
return EnvValue.StringValue("")
}

private fun parseShell(keyValue: YAMLKeyValue): String {
Expand Down Expand Up @@ -98,13 +145,19 @@ class Config(
}
}

private fun parseInit(keyValue: YAMLKeyValue): String {
return when (val value = keyValue.value) {
is YAMLScalar -> value.textValue
else -> ""
}
}

@Suppress("NestedBlockDepth")
private fun parseCommand(keyValue: YAMLKeyValue): Command {
val name = keyValue.keyText
var cmd = ""
var cmdAsMap = emptyMap<String, String>()
var env: Env = emptyMap()
var evalEnv: Env = emptyMap()
var depends = emptyList<String>()

when (val value = keyValue.value) {
Expand All @@ -129,9 +182,6 @@ class Config(
"env" -> {
env = parseEnv(kv)
}
"eval_env" -> {
evalEnv = parseEnv(kv)
}
"depends" -> {
depends = parseDepends(kv)
}
Expand All @@ -145,36 +195,54 @@ class Config(
cmd,
cmdAsMap,
env,
evalEnv,
depends,
)
}

@Suppress("NestedBlockDepth")
private fun parseConfigFromMapping(mapping: YAMLMapping): Config {
var shell = ""
val mixins = mutableListOf<Mixin>()
val commands = mutableListOf<Command>()
val commandsMap = mutableMapOf<String, Command>()
var env: Env = emptyMap()
var evalEnv: Env = emptyMap()
var before = ""
val specifiedDirectives = mutableSetOf<String>()
var init = ""
val keywordsInConfig = mutableSetOf<String>()

mapping.keyValues.forEach {
kv ->
when (kv.keyText) {
"shell" -> {
shell = parseShell(kv)
}
"mixins" -> {
when (val value = kv.value) {
is YAMLSequence -> {
mixins.addAll(
value.items.mapNotNull { it.value }
.map { when (it) {
is YAMLScalar -> Mixin.Local(it.textValue)
is YAMLMapping -> {
val url = it.getKeyValueByKey("url")?.valueText ?: ""
val version = it.getKeyValueByKey("version")?.valueText ?: ""
Mixin.Remote(url, version)
}
else -> Mixin.Local("")
} }
)
}
}
}
"env" -> {
env = parseEnv(kv)
}
"eval_env" -> {
evalEnv = parseEnv(kv)
}
"before" -> {
before = parseBefore(kv)
}
"init" -> {
init = parseInit(kv)
}
"commands" -> {
when (val value = kv.value) {
is YAMLMapping -> {
Expand All @@ -187,17 +255,18 @@ class Config(
}
}
}
specifiedDirectives.add(kv.keyText)
keywordsInConfig.add(kv.keyText)
}

return Config(
shell,
commands,
commandsMap,
env,
evalEnv,
before,
specifiedDirectives,
init,
mixins,
keywordsInConfig,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ object LetsCompletionProvider : CompletionProvider<CompletionParameters>() {

when {
LetsCompletionHelper.isRootLevel(parameters) -> {
val suggestions = when (config.specifiedDirectives.size) {
val suggestions = when (config.keywordsInConfig.size) {
0 -> TOP_LEVEL_KEYWORDS
else -> TOP_LEVEL_KEYWORDS.filterNot { config.specifiedDirectives.contains(it) }.toList()
else -> TOP_LEVEL_KEYWORDS.filterNot { config.keywordsInConfig.contains(it) }.toList()
}
result.addAllElements(
suggestions.map { keyword ->
when (keyword) {
"commands", "env", "eval_env" -> createRootKeyNewLineElement(keyword)
"before" -> createRootKeyMultilineElement(keyword)
"commands", "env" -> createRootKeyNewLineElement(keyword)
"before", "init" -> createRootKeyMultilineElement(keyword)
"mixins" -> createRootKeyArrayElement(keyword)
else -> createRootKeyElement(keyword)
}
Expand All @@ -57,7 +57,7 @@ object LetsCompletionProvider : CompletionProvider<CompletionParameters>() {
when (keyword) {
"options" -> createOptionsElement()
"depends" -> createDependsElement()
"env", "eval_env" -> createCommandKeyNewLineElement(keyword)
"env" -> createCommandKeyNewLineElement(keyword)
else -> createCommandKeyElement(keyword)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(ele

// Collect all YAML files, including in subdirectories
val yamlFiles = FilenameIndex.getAllFilesByExt(project, "yaml")
.mapNotNull { file -> file.path?.let { LookupElementBuilder.create(it.removePrefix(project.basePath ?: "")) } }
.mapNotNull { file -> file.path.let { LookupElementBuilder.create(it.removePrefix(project.basePath ?: "")) } }

return yamlFiles.toTypedArray()
}
Expand All @@ -65,8 +65,10 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(ele
* Supports both top-level files ("lets.build.yaml") and nested files ("lets/lets.docs.yaml").
*/
private fun findMixinFile(project: Project, mixinPath: String): VirtualFile? {
// Normalize paths (handle both "lets.build.yaml" and "lets/lets.docs.yaml")
// 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")
.removePrefix("-")

// Look for an exact match in the project
return FilenameIndex.getVirtualFilesByName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ val DEFAULT_SHELLS = listOf(
val TOP_LEVEL_KEYWORDS = listOf(
"shell",
"before",
"init",
"commands",
"env",
"eval_env",
"version",
"mixins"
)

val COMMAND_LEVEL_KEYWORDS = listOf(
"description",
"env",
"eval_env",
"options",
"checksum",
"persist_checksum",
"cmd",
"work_dir",
"depends",
"after"
"after",
"ref",
"args",
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ open class FieldsTest : BasePlatformTestCase() {

val expected = listOf(
"env",
"eval_env",
"version",
"mixins",
"before"
"before",
"init"
)

assertEquals(expected.sorted(), variants?.sorted())
Expand Down
Loading