Skip to content

Commit 4b9a148

Browse files
authored
Merge pull request #40 from lets-cli/improve-config-parser
improve config parser and add support for modern lets keywords
2 parents 978bea8 + 9a4fe1a commit 4b9a148

File tree

11 files changed

+292
-55
lines changed

11 files changed

+292
-55
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
# intellij-lets Changelog
44

55
## [Unreleased]
6+
### Updated
7+
8+
- make go to definition work for gitignore `mixins` files (with '-' (dash) at the beginning)
9+
- support global `init` in config parser and completion
10+
- support `sh` and `checksum` modes in `env` in config parser
11+
- support `ref` and `args` in command keyword completion
12+
- support `mixins` (both local and remote) in config parser
13+
- drop `eval_env`
14+
15+
### Chore
16+
17+
- add tests for reference contributor
618

719
## [0.0.16] - 2025-03-07
820

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ File type recognition for `lets.yaml` and `lets.*.yaml` configs
1313

1414
- **Completion**
1515
- Complete keywords
16-
- Complete command `options` with code snippet
17-
- Complete commands in `depends` with code snippet
16+
- [x] Complete command `options` with code snippet
17+
- [x] Complete commands in `depends` with code snippet
18+
- [ ] Complete commands in `depends` from mixins
19+
- [ ] Complete env mode in `env` with code snippet
1820
- **Go To Definition**
19-
- Navigate to definitions of `mixins` files
21+
- [x] Navigate to definitions of `mixins` files
22+
- [x] Navigate to definitions of optional `mixins` files (with - at the beginning)
23+
- [x] Navigate to definitions of `mixins` remote files (as http links)
24+
- [ ] Navigate to definitions of commands in `depends`
25+
- [ ] Navigate to definitions of commands in `depends` from mixins
26+
- **Highlighting**
27+
- [ ] Highlighting for shell script in cmd
2028

2129
<!-- Plugin description end -->
2230

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

101109
## Todo
102110

103-
- add highlighting for shell script in cmd - https://plugins.jetbrains.com/docs/intellij/file-view-providers.html
104-
- 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
105111
- insert not padded strings, but yaml elements

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

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@ import org.jetbrains.yaml.psi.YAMLMapping
77
import org.jetbrains.yaml.psi.YAMLScalar
88
import org.jetbrains.yaml.psi.YAMLSequence
99

10-
typealias Env = Map<String, String>
10+
sealed class EnvValue {
11+
data class StringValue(val value: String) : EnvValue()
12+
data class ShMode(val sh: String) : EnvValue()
13+
data class ChecksumMode(val files: List<String>) : EnvValue()
14+
data class ChecksumMapMode(val files: Map<String, List<String>>) : EnvValue()
15+
}
16+
17+
typealias Env = Map<String, EnvValue>
18+
19+
sealed class Mixin {
20+
data class Local(val path: String) : Mixin()
21+
data class Remote(val url: String, val version: String) : Mixin()
22+
}
23+
24+
typealias Mixins = List<Mixin>
1125

1226
data class Command(
1327
val name: String,
1428
val cmd: String,
1529
val cmdAsMap: Map<String, String>,
1630
val env: Env,
17-
val evalEnv: Env,
1831
val depends: List<String>,
1932
)
2033

@@ -34,9 +47,11 @@ class Config(
3447
val commands: List<Command>,
3548
val commandsMap: Map<String, Command>,
3649
val env: Env,
37-
val evalEnv: Env,
3850
val before: String,
39-
val specifiedDirectives: Set<String>,
51+
val init: String,
52+
val mixins: Mixins,
53+
// Keywords that are used in the config
54+
val keywordsInConfig: Set<String>,
4055
) {
4156

4257
companion object Parser {
@@ -59,17 +74,49 @@ class Config(
5974
emptyList(),
6075
emptyMap(),
6176
emptyMap(),
62-
emptyMap(),
6377
"",
78+
"",
79+
emptyList(),
6480
emptySet(),
6581
)
6682
}
6783

6884
private fun parseEnv(keyValue: YAMLKeyValue): Env {
69-
return when (val value = keyValue.value) {
70-
is YAMLMapping -> value.keyValues.associate { kv -> kv.keyText to kv.valueText }
71-
else -> emptyMap()
85+
val value = keyValue.value as? YAMLMapping ?: return emptyMap()
86+
87+
return value.keyValues.associate { kv ->
88+
kv.keyText to parseEnvValue(kv)
89+
}
90+
}
91+
92+
private fun parseEnvValue(kv: YAMLKeyValue): EnvValue {
93+
return when (val envValue = kv.value) {
94+
is YAMLScalar -> EnvValue.StringValue(envValue.textValue)
95+
is YAMLMapping -> parseMappingEnvValue(envValue)
96+
else -> EnvValue.StringValue("")
97+
}
98+
}
99+
100+
private fun parseMappingEnvValue(value: YAMLMapping): EnvValue {
101+
value.keyValues.forEach { kv ->
102+
when (kv.keyText) {
103+
"sh" -> return EnvValue.ShMode(kv.valueText)
104+
"checksum" -> {
105+
return when (val checksumValue = kv.value) {
106+
is YAMLSequence -> EnvValue.ChecksumMode(
107+
checksumValue.items.mapNotNull { it.value?.text }
108+
)
109+
is YAMLMapping -> EnvValue.ChecksumMapMode(
110+
checksumValue.keyValues.associate { entry ->
111+
entry.keyText to (entry.value as YAMLSequence).items.mapNotNull { it.value?.text }
112+
}
113+
)
114+
else -> EnvValue.StringValue("")
115+
}
116+
}
117+
}
72118
}
119+
return EnvValue.StringValue("")
73120
}
74121

75122
private fun parseShell(keyValue: YAMLKeyValue): String {
@@ -98,13 +145,19 @@ class Config(
98145
}
99146
}
100147

148+
private fun parseInit(keyValue: YAMLKeyValue): String {
149+
return when (val value = keyValue.value) {
150+
is YAMLScalar -> value.textValue
151+
else -> ""
152+
}
153+
}
154+
101155
@Suppress("NestedBlockDepth")
102156
private fun parseCommand(keyValue: YAMLKeyValue): Command {
103157
val name = keyValue.keyText
104158
var cmd = ""
105159
var cmdAsMap = emptyMap<String, String>()
106160
var env: Env = emptyMap()
107-
var evalEnv: Env = emptyMap()
108161
var depends = emptyList<String>()
109162

110163
when (val value = keyValue.value) {
@@ -129,9 +182,6 @@ class Config(
129182
"env" -> {
130183
env = parseEnv(kv)
131184
}
132-
"eval_env" -> {
133-
evalEnv = parseEnv(kv)
134-
}
135185
"depends" -> {
136186
depends = parseDepends(kv)
137187
}
@@ -145,36 +195,54 @@ class Config(
145195
cmd,
146196
cmdAsMap,
147197
env,
148-
evalEnv,
149198
depends,
150199
)
151200
}
152201

153202
@Suppress("NestedBlockDepth")
154203
private fun parseConfigFromMapping(mapping: YAMLMapping): Config {
155204
var shell = ""
205+
val mixins = mutableListOf<Mixin>()
156206
val commands = mutableListOf<Command>()
157207
val commandsMap = mutableMapOf<String, Command>()
158208
var env: Env = emptyMap()
159-
var evalEnv: Env = emptyMap()
160209
var before = ""
161-
val specifiedDirectives = mutableSetOf<String>()
210+
var init = ""
211+
val keywordsInConfig = mutableSetOf<String>()
162212

163213
mapping.keyValues.forEach {
164214
kv ->
165215
when (kv.keyText) {
166216
"shell" -> {
167217
shell = parseShell(kv)
168218
}
219+
"mixins" -> {
220+
when (val value = kv.value) {
221+
is YAMLSequence -> {
222+
mixins.addAll(
223+
value.items.mapNotNull { it.value }
224+
.map { when (it) {
225+
is YAMLScalar -> Mixin.Local(it.textValue)
226+
is YAMLMapping -> {
227+
val url = it.getKeyValueByKey("url")?.valueText ?: ""
228+
val version = it.getKeyValueByKey("version")?.valueText ?: ""
229+
Mixin.Remote(url, version)
230+
}
231+
else -> Mixin.Local("")
232+
} }
233+
)
234+
}
235+
}
236+
}
169237
"env" -> {
170238
env = parseEnv(kv)
171239
}
172-
"eval_env" -> {
173-
evalEnv = parseEnv(kv)
174-
}
175240
"before" -> {
176241
before = parseBefore(kv)
177242
}
243+
"init" -> {
244+
init = parseInit(kv)
245+
}
178246
"commands" -> {
179247
when (val value = kv.value) {
180248
is YAMLMapping -> {
@@ -187,17 +255,18 @@ class Config(
187255
}
188256
}
189257
}
190-
specifiedDirectives.add(kv.keyText)
258+
keywordsInConfig.add(kv.keyText)
191259
}
192260

193261
return Config(
194262
shell,
195263
commands,
196264
commandsMap,
197265
env,
198-
evalEnv,
199266
before,
200-
specifiedDirectives,
267+
init,
268+
mixins,
269+
keywordsInConfig,
201270
)
202271
}
203272
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ object LetsCompletionProvider : CompletionProvider<CompletionParameters>() {
3333

3434
when {
3535
LetsCompletionHelper.isRootLevel(parameters) -> {
36-
val suggestions = when (config.specifiedDirectives.size) {
36+
val suggestions = when (config.keywordsInConfig.size) {
3737
0 -> TOP_LEVEL_KEYWORDS
38-
else -> TOP_LEVEL_KEYWORDS.filterNot { config.specifiedDirectives.contains(it) }.toList()
38+
else -> TOP_LEVEL_KEYWORDS.filterNot { config.keywordsInConfig.contains(it) }.toList()
3939
}
4040
result.addAllElements(
4141
suggestions.map { keyword ->
4242
when (keyword) {
43-
"commands", "env", "eval_env" -> createRootKeyNewLineElement(keyword)
44-
"before" -> createRootKeyMultilineElement(keyword)
43+
"commands", "env" -> createRootKeyNewLineElement(keyword)
44+
"before", "init" -> createRootKeyMultilineElement(keyword)
4545
"mixins" -> createRootKeyArrayElement(keyword)
4646
else -> createRootKeyElement(keyword)
4747
}
@@ -57,7 +57,7 @@ object LetsCompletionProvider : CompletionProvider<CompletionParameters>() {
5757
when (keyword) {
5858
"options" -> createOptionsElement()
5959
"depends" -> createDependsElement()
60-
"env", "eval_env" -> createCommandKeyNewLineElement(keyword)
60+
"env" -> createCommandKeyNewLineElement(keyword)
6161
else -> createCommandKeyElement(keyword)
6262
}
6363
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(ele
5555

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

6060
return yamlFiles.toTypedArray()
6161
}
@@ -65,8 +65,10 @@ class LetsMixinReference(element: YAMLScalar) : PsiReferenceBase<YAMLScalar>(ele
6565
* Supports both top-level files ("lets.build.yaml") and nested files ("lets/lets.docs.yaml").
6666
*/
6767
private fun findMixinFile(project: Project, mixinPath: String): VirtualFile? {
68-
// Normalize paths (handle both "lets.build.yaml" and "lets/lets.docs.yaml")
68+
// Normalize paths (handle both "lets.mixin.yaml" and "lets/lets.mixin.yaml")
6969
val normalizedPath = mixinPath.trimStart('/')
70+
// Normalize gitignored files (e.g. "-lets.mixin.yaml" -> "lets.mixin.yaml")
71+
.removePrefix("-")
7072

7173
// Look for an exact match in the project
7274
return FilenameIndex.getVirtualFilesByName(

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@ val DEFAULT_SHELLS = listOf(
1414
val TOP_LEVEL_KEYWORDS = listOf(
1515
"shell",
1616
"before",
17+
"init",
1718
"commands",
1819
"env",
19-
"eval_env",
2020
"version",
2121
"mixins"
2222
)
2323

2424
val COMMAND_LEVEL_KEYWORDS = listOf(
2525
"description",
2626
"env",
27-
"eval_env",
2827
"options",
2928
"checksum",
3029
"persist_checksum",
3130
"cmd",
31+
"work_dir",
3232
"depends",
33-
"after"
33+
"after",
34+
"ref",
35+
"args",
3436
)

src/test/kotlin/com/github/kindermax/intellijlets/completion/field/FieldsTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ open class FieldsTest : BasePlatformTestCase() {
2323

2424
val expected = listOf(
2525
"env",
26-
"eval_env",
2726
"version",
2827
"mixins",
29-
"before"
28+
"before",
29+
"init"
3030
)
3131

3232
assertEquals(expected.sorted(), variants?.sorted())

0 commit comments

Comments
 (0)