From ad7370dc01085072eb0a7e96fbdda05aacb42e0d Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sat, 8 Mar 2025 23:59:03 +0200 Subject: [PATCH 1/5] Complete env vars from global env in cmd scripts --- README.md | 2 +- .../LetsEnvVariableCompletionContributor.kt | 21 ++++++++++++++---- .../kindermax/intellijlets/LetsPsiUtils.kt | 10 +++++++++ .../intellijlets/LetsPsiUtilsTest.kt | 21 ++++++++++++++++++ .../completion/CompleteEnvTest.kt | 22 +++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index aff2b7d..a579931 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 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 + - [x] 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 diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt index d12ceda..8a88b42 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt @@ -10,6 +10,7 @@ import com.intellij.util.ProcessingContext import org.jetbrains.yaml.psi.YAMLKeyValue import org.jetbrains.yaml.psi.YAMLScalar import org.jetbrains.yaml.psi.YAMLMapping +import org.jetbrains.yaml.psi.YAMLFile import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.psi.PsiFile @@ -24,6 +25,7 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { } protected fun completeCmdEnvVariables( + parameters: CompletionParameters, result: CompletionResultSet, cmdKeyValue: YAMLKeyValue?, prefixText: String, @@ -38,14 +40,15 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { val extractedOptions = extractOptionNames(optionsText) when { - prefixText.endsWith("$") -> addEnvVariableCompletions(result, "$", extractedOptions) - prefixText.endsWith("\$L") -> addEnvVariableCompletions(result, "\$L", extractedOptions) - prefixText.endsWith("\${") -> addEnvVariableCompletions(result, "\${", extractedOptions) - prefixText.endsWith("\${L") -> addEnvVariableCompletions(result, "\${L", extractedOptions) + prefixText.endsWith("$") -> addEnvVariableCompletions(parameters, result, "$", extractedOptions) + prefixText.endsWith("\$L") -> addEnvVariableCompletions(parameters, result, "\$L", extractedOptions) + prefixText.endsWith("\${") -> addEnvVariableCompletions(parameters, result, "\${", extractedOptions) + prefixText.endsWith("\${L") -> addEnvVariableCompletions(parameters, result, "\${L", extractedOptions) } } private fun addEnvVariableCompletions( + parameters: CompletionParameters, result: CompletionResultSet, prefix: String, extractedOptions: Set @@ -56,6 +59,14 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { prefixMatcher.addElement(createEnvVariableLookupElement(it)) } + val globalEnvVars = (parameters.originalFile as? YAMLFile)?.let { + LetsPsiUtils.getGlobalEnvVariables(it) + } ?: emptySet() + + globalEnvVars.forEach { + prefixMatcher.addElement(createEnvVariableLookupElement(it)) + } + extractedOptions.forEach { option -> prefixMatcher.addElement(createEnvVariableLookupElement("LETSOPT_${option.uppercase()}")) prefixMatcher.addElement(createEnvVariableLookupElement("LETSCLI_${option.uppercase()}")) @@ -109,6 +120,7 @@ class LetsEnvVariableCompletionContributor : LetsEnvVariableCompletionContributo val prefixText = parameters.editor.document.getText(TextRange(lineOffset, caret.offset)) completeCmdEnvVariables( + parameters, result, keyValue, prefixText, @@ -160,6 +172,7 @@ class LetsEnvVariableShellScriptCompletionContributor : LetsEnvVariableCompletio val prefixText = parameters.editor.document.getText(TextRange(parameters.offset - 1, parameters.offset)) completeCmdEnvVariables( + parameters, result, keyValue, prefixText, diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt index b60ef28..24b7039 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt @@ -75,4 +75,14 @@ object LetsPsiUtils { return usedKeywords } + + fun getGlobalEnvVariables(yamlFile: YAMLFile): List { + val envKey = PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java) + .firstOrNull { it.keyText == "env" } ?: return emptyList() + + return (envKey.value as? YAMLMapping) + ?.keyValues + ?.mapNotNull { it.keyText } + ?: emptyList() + } } \ No newline at end of file diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt index 67d0f4f..3bf9de7 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt @@ -193,4 +193,25 @@ class LetsPsiUtilsTest : BasePlatformTestCase() { val usedKeywords = LetsPsiUtils.getUsedKeywords(file as YAMLFile) assertEquals(usedKeywords.sorted(), listOf("shell", "mixins", "before", "commands").sorted()) } + + fun testFindGlobalEnv() { + val file = myFixture.configureByText( + "lets.yaml", + """ + shell: bash + env: + OS: Darwin + DEV: true + + commands: + world: + cmd: echo World + env: + FOO: BAR + """.trimIndent() + ) + + val envKeys = LetsPsiUtils.getGlobalEnvVariables(file as YAMLFile) + assertEquals(envKeys.toSet(), setOf("OS", "DEV")) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt index bbb7d78..3ce9e67 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt @@ -46,4 +46,26 @@ open class CompleteEnvTest : BasePlatformTestCase() { assertEquals(expected, variants?.toSet()) } + + fun testCompleteFromGlobalEnv() { + myFixture.configureByText( + "lets.yaml", + """ + shell: bash + env: + DEV: true + + commands: + run: + cmd: Echo $ + """.trimIndent() + ) + val variants = myFixture.getCompletionVariants("lets.yaml") + assertNotNull(variants) + + val expected = BUILTIN_ENV_VARIABLES.map { "\${$it}" }.toMutableSet() + expected.add("\${DEV}") + + assertEquals(expected, variants?.toSet()) + } } From 745179d08bcc33af631dc35f7b032c5e06bf3096 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sun, 9 Mar 2025 00:02:19 +0200 Subject: [PATCH 2/5] Complete env vars from command env in cmd scripts --- .../LetsEnvVariableCompletionContributor.kt | 12 +++++++--- .../completion/CompleteEnvTest.kt | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt index 8a88b42..8e4a646 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt @@ -59,14 +59,20 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { prefixMatcher.addElement(createEnvVariableLookupElement(it)) } - val globalEnvVars = (parameters.originalFile as? YAMLFile)?.let { - LetsPsiUtils.getGlobalEnvVariables(it) - } ?: emptySet() + val currentFile = parameters.originalFile as YAMLFile + + val globalEnvVars = LetsPsiUtils.getGlobalEnvVariables(currentFile) globalEnvVars.forEach { prefixMatcher.addElement(createEnvVariableLookupElement(it)) } + val currentCommand = LetsPsiUtils.findCurrentCommand(currentFile) + + currentCommand?.env?.keys?.forEach { + prefixMatcher.addElement(createEnvVariableLookupElement(it)) + } + extractedOptions.forEach { option -> prefixMatcher.addElement(createEnvVariableLookupElement("LETSOPT_${option.uppercase()}")) prefixMatcher.addElement(createEnvVariableLookupElement("LETSCLI_${option.uppercase()}")) diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt index 3ce9e67..754e657 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteEnvTest.kt @@ -68,4 +68,26 @@ open class CompleteEnvTest : BasePlatformTestCase() { assertEquals(expected, variants?.toSet()) } + + fun testCompleteFromCommandEnv() { + myFixture.configureByText( + "lets.yaml", + """ + shell: bash + + commands: + run: + env: + DEV: true + cmd: Echo $ + """.trimIndent() + ) + val variants = myFixture.getCompletionVariants("lets.yaml") + assertNotNull(variants) + + val expected = BUILTIN_ENV_VARIABLES.map { "\${$it}" }.toMutableSet() + expected.add("\${DEV}") + + assertEquals(expected, variants?.toSet()) + } } From 8c69410dfd88a6a7724afc653d98982cc1ce1573 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sun, 9 Mar 2025 00:27:09 +0200 Subject: [PATCH 3/5] Pass YAMLFile to completeCmdEnvVariables --- .../intellijlets/LetsCompletionHelper.kt | 1 - .../LetsEnvVariableCompletionContributor.kt | 25 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt index 4b6187a..3d12a81 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt @@ -44,7 +44,6 @@ object LetsCompletionHelper { private fun isInCommandKey(keyValue: YAMLKeyValue): Boolean { val parentKeyValue = keyValue.parent?.parent as? YAMLKeyValue ?: return false return parentKeyValue.keyText == "commands" -// && keyValue.keyText in COMMAND_LEVEL_KEYWORDS } /** diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt index 8e4a646..2f4eb35 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt @@ -25,7 +25,7 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { } protected fun completeCmdEnvVariables( - parameters: CompletionParameters, + yamlFile: YAMLFile, result: CompletionResultSet, cmdKeyValue: YAMLKeyValue?, prefixText: String, @@ -40,15 +40,15 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { val extractedOptions = extractOptionNames(optionsText) when { - prefixText.endsWith("$") -> addEnvVariableCompletions(parameters, result, "$", extractedOptions) - prefixText.endsWith("\$L") -> addEnvVariableCompletions(parameters, result, "\$L", extractedOptions) - prefixText.endsWith("\${") -> addEnvVariableCompletions(parameters, result, "\${", extractedOptions) - prefixText.endsWith("\${L") -> addEnvVariableCompletions(parameters, result, "\${L", extractedOptions) + prefixText.endsWith("$") -> addEnvVariableCompletions(yamlFile, result, "$", extractedOptions) + prefixText.endsWith("\$L") -> addEnvVariableCompletions(yamlFile, result, "\$L", extractedOptions) + prefixText.endsWith("\${") -> addEnvVariableCompletions(yamlFile, result, "\${", extractedOptions) + prefixText.endsWith("\${L") -> addEnvVariableCompletions(yamlFile, result, "\${L", extractedOptions) } } private fun addEnvVariableCompletions( - parameters: CompletionParameters, + yamlFile: YAMLFile, result: CompletionResultSet, prefix: String, extractedOptions: Set @@ -59,15 +59,13 @@ open class LetsEnvVariableCompletionContributorBase : CompletionContributor() { prefixMatcher.addElement(createEnvVariableLookupElement(it)) } - val currentFile = parameters.originalFile as YAMLFile - - val globalEnvVars = LetsPsiUtils.getGlobalEnvVariables(currentFile) + val globalEnvVars = LetsPsiUtils.getGlobalEnvVariables(yamlFile) globalEnvVars.forEach { prefixMatcher.addElement(createEnvVariableLookupElement(it)) } - val currentCommand = LetsPsiUtils.findCurrentCommand(currentFile) + val currentCommand = LetsPsiUtils.findCurrentCommand(yamlFile) currentCommand?.env?.keys?.forEach { prefixMatcher.addElement(createEnvVariableLookupElement(it)) @@ -112,6 +110,7 @@ class LetsEnvVariableCompletionContributor : LetsEnvVariableCompletionContributo val element = parameters.position val keyValue = PsiTreeUtil.getParentOfType(element, YAMLKeyValue::class.java) ?: return + val currentFile = parameters.originalFile as YAMLFile when (keyValue.keyText) { "options" -> { @@ -126,7 +125,7 @@ class LetsEnvVariableCompletionContributor : LetsEnvVariableCompletionContributo val prefixText = parameters.editor.document.getText(TextRange(lineOffset, caret.offset)) completeCmdEnvVariables( - parameters, + currentFile, result, keyValue, prefixText, @@ -163,7 +162,7 @@ class LetsEnvVariableShellScriptCompletionContributor : LetsEnvVariableCompletio val yamlFile: PsiFile = injectedLanguageManager.getInjectionHost(element)?.containingFile ?: return // Ensure it's a YAML file - if (yamlFile !is org.jetbrains.yaml.psi.YAMLFile) return + if (yamlFile !is YAMLFile) return // Retrieve the correct offset in the original YAML file val hostOffset = injectedLanguageManager.injectedToHost(element, element.textOffset) @@ -178,7 +177,7 @@ class LetsEnvVariableShellScriptCompletionContributor : LetsEnvVariableCompletio val prefixText = parameters.editor.document.getText(TextRange(parameters.offset - 1, parameters.offset)) completeCmdEnvVariables( - parameters, + yamlFile, result, keyValue, prefixText, From 00060533e716a21227f265843cb1a5db7128aadf Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sun, 9 Mar 2025 01:12:41 +0200 Subject: [PATCH 4/5] Refactor LetsCompletionProvider * Move YamlContextType to LetsPsiUtils * Drop LetsCompletionHelper.kt * Implement env modes completions * Add command name to *options* completion text --- README.md | 2 +- .../intellijlets/LetsCompletionHelper.kt | 92 ---------- .../intellijlets/LetsCompletionProvider.kt | 162 ++++++++++++++++-- .../kindermax/intellijlets/LetsPsiUtils.kt | 49 +++++- .../intellijlets/LetsPsiUtilsTest.kt | 22 +++ .../completion/CompleteKeywordTest.kt | 10 +- .../completion/DetectContextTest.kt | 23 +-- 7 files changed, 236 insertions(+), 124 deletions(-) delete mode 100644 src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt diff --git a/README.md b/README.md index a579931..1dfc2be 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ File type recognition for `lets.yaml` and `lets.*.yaml` configs - [x] Complete command `options` with code snippet - [x] Complete commands in `depends` with code snippet - [x] Complete commands in `depends` from mixins - - [ ] Complete env mode in `env` with code snippet + - [x] Complete env mode in `env` with code snippet - [x] Complete `LETS*` environment variables in cmd scripts - [ ] Complete environment variables for checksum - [x] Complete environment variables from global and command `env` in cmd scripts diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt deleted file mode 100644 index 3d12a81..0000000 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionHelper.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.github.kindermax.intellijlets - -import com.intellij.codeInsight.completion.CompletionParameters -import com.intellij.psi.PsiElement -import com.intellij.psi.util.PsiTreeUtil -import org.jetbrains.yaml.psi.YAMLFile -import org.jetbrains.yaml.psi.YAMLKeyValue - -object LetsCompletionHelper { - sealed class YamlContextType { - object RootLevel : YamlContextType() - object CommandLevel : YamlContextType() - object ShellLevel : YamlContextType() - object DependsLevel : YamlContextType() - object RefLevel : YamlContextType() - object Unknown : YamlContextType() - } - - fun detectContext(position: PsiElement): YamlContextType { - if (isRootLevel(position)) return YamlContextType.RootLevel - - val keyValue = PsiTreeUtil.getParentOfType(position, YAMLKeyValue::class.java) ?: return YamlContextType.Unknown - - return when { - isInCommandKey(keyValue) -> YamlContextType.CommandLevel - isInTopLevelKey(keyValue) -> YamlContextType.RootLevel - keyValue.keyText == "shell" -> YamlContextType.ShellLevel - keyValue.keyText == "depends" -> YamlContextType.DependsLevel - keyValue.keyText == "ref" -> YamlContextType.RefLevel - else -> YamlContextType.Unknown - } - } - - private fun isRootLevel(position: PsiElement): Boolean { - return ( - position.parent.parent.parent is YAMLFile || - position.parent.parent.parent.parent is YAMLFile - ) - } - private fun isInTopLevelKey(keyValue: YAMLKeyValue): Boolean { - return keyValue.keyText in TOP_LEVEL_KEYWORDS && keyValue.parent?.parent is YAMLFile - } - - private fun isInCommandKey(keyValue: YAMLKeyValue): Boolean { - val parentKeyValue = keyValue.parent?.parent as? YAMLKeyValue ?: return false - return parentKeyValue.keyText == "commands" - } - - /** - * Get all possible commands suggestions for a `depends`, except: - * - itself - * - already specified commands in depends - * - other commands which depend on current command - */ - fun getDependsSuggestions(parameters: CompletionParameters): List { - val yamlFile = parameters.originalFile as? YAMLFile ?: return emptyList() - val allCommands = LetsPsiUtils.findAllCommands(yamlFile) - val currentCommand = LetsPsiUtils.findCurrentCommand(parameters.position) ?: return emptyList() - - val excludeList = mutableSetOf() - // exclude itself - excludeList.add(currentCommand.name) - // exclude commands already in depends list - excludeList.addAll(currentCommand.depends) - - // exclude commands which depends on current command (eliminate recursive dependencies) - for (command in allCommands.filter { c -> c.name != currentCommand.name }) { - if (command.depends.contains(currentCommand.name)) { - excludeList.add(command.name) - } - } - - return allCommands - .filterNot { command -> excludeList.contains(command.name) } - .map { it.name } - .toList() - } - - /** - * Get all possible commands suggestions for a `ref`, except: - * - itself - * Since ref is a YAMLScalar, only one command is suggested. - */ - fun getRefSuggestions(parameters: CompletionParameters): List { - val yamlFile = parameters.originalFile as? YAMLFile ?: return emptyList() - val allCommands = LetsPsiUtils.findAllCommands(yamlFile) - val currentCommand = LetsPsiUtils.findCurrentCommand(parameters.position) ?: return emptyList() - // Exclude itself from suggestions and return only one suggestion - return allCommands.filterNot { it.name == currentCommand.name } - .map { it.name } - } -} diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionProvider.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionProvider.kt index e5f3c56..1778e9e 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionProvider.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsCompletionProvider.kt @@ -7,6 +7,8 @@ import com.intellij.codeInsight.completion.InsertHandler import com.intellij.codeInsight.completion.InsertionContext import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.template.TemplateManager +import com.intellij.codeInsight.template.impl.TextExpression import com.intellij.util.ProcessingContext import org.jetbrains.yaml.psi.YAMLFile @@ -17,8 +19,8 @@ object LetsCompletionProvider : CompletionProvider() { context: ProcessingContext, result: CompletionResultSet, ) { - when (LetsCompletionHelper.detectContext(parameters.position)) { - LetsCompletionHelper.YamlContextType.RootLevel -> { + when (LetsPsiUtils.detectContext(parameters.position)) { + YamlContextType.RootLevel -> { val yamlFile = parameters.originalFile as YAMLFile val usedKeywords = LetsPsiUtils.getUsedKeywords(yamlFile) val suggestions = when (usedKeywords.size) { @@ -36,14 +38,17 @@ object LetsCompletionProvider : CompletionProvider() { } ) } - LetsCompletionHelper.YamlContextType.ShellLevel -> { + + YamlContextType.ShellLevel -> { result.addAllElements(DEFAULT_SHELLS.map { keyword -> createLookupElement(keyword) }) } - LetsCompletionHelper.YamlContextType.CommandLevel -> { + + YamlContextType.CommandLevel -> { + val currentCommand = LetsPsiUtils.findCurrentCommand(parameters.position, YamlContextType.CommandLevel) result.addAllElements( COMMAND_LEVEL_KEYWORDS.map { keyword -> when (keyword) { - "options" -> createOptionsElement() + "options" -> createOptionsElement(currentCommand?.name) "depends" -> createDependsElement() "env" -> createCommandKeyNewLineElement(keyword) else -> createCommandKeyElement(keyword) @@ -51,21 +56,77 @@ object LetsCompletionProvider : CompletionProvider() { } ) } - LetsCompletionHelper.YamlContextType.DependsLevel -> { - val suggestions = LetsCompletionHelper.getDependsSuggestions(parameters) + + YamlContextType.DependsLevel -> { + val suggestions = getDependsSuggestions(parameters) result.addAllElements( suggestions.map { keyword -> createLookupElement(keyword) } ) } - LetsCompletionHelper.YamlContextType.RefLevel -> { - val suggestions = LetsCompletionHelper.getRefSuggestions(parameters) + + YamlContextType.RefLevel -> { + val suggestions = getRefSuggestions(parameters) result.addAllElements( suggestions.map { keyword -> createLookupElement(keyword) } ) } - LetsCompletionHelper.YamlContextType.Unknown -> return + + YamlContextType.EnvLevel -> { + result.addAllElements( + listOf( + createEnvStringElement(), + createEnvShellElement() + ) + ) + } + + YamlContextType.Unknown -> return } } + + /** + * Get all possible commands suggestions for a `depends`, except: + * - itself + * - already specified commands in depends + * - other commands which depend on current command + */ + private fun getDependsSuggestions(parameters: CompletionParameters): List { + val yamlFile = parameters.originalFile as? YAMLFile ?: return emptyList() + val allCommands = LetsPsiUtils.findAllCommands(yamlFile) + val currentCommand = LetsPsiUtils.findCurrentCommand(parameters.position) ?: return emptyList() + + val excludeList = mutableSetOf() + // exclude itself + excludeList.add(currentCommand.name) + // exclude commands already in depends list + excludeList.addAll(currentCommand.depends) + + // exclude commands which depends on current command (eliminate recursive dependencies) + for (command in allCommands.filter { c -> c.name != currentCommand.name }) { + if (command.depends.contains(currentCommand.name)) { + excludeList.add(command.name) + } + } + + return allCommands + .filterNot { command -> excludeList.contains(command.name) } + .map { it.name } + .toList() + } + + /** + * Get all possible commands suggestions for a `ref`, except: + * - itself + * Since ref is a YAMLScalar, only one command is suggested. + */ + private fun getRefSuggestions(parameters: CompletionParameters): List { + val yamlFile = parameters.originalFile as? YAMLFile ?: return emptyList() + val allCommands = LetsPsiUtils.findAllCommands(yamlFile) + val currentCommand = LetsPsiUtils.findCurrentCommand(parameters.position) ?: return emptyList() + // Exclude itself from suggestions and return only one suggestion + return allCommands.filterNot { it.name == currentCommand.name } + .map { it.name } + } } private fun createLookupElement(text: String): LookupElement { @@ -116,11 +177,11 @@ private fun createCommandKeyNewLineElement(text: String): LookupElement { .withInsertHandler(CommandKeyInsertionHandler(newLine = true)) } -private fun createOptionsElement(): LookupElement { +private fun createOptionsElement(name: String?): LookupElement { return LookupElementBuilder .create("options") .withIcon(Icons.LetsYaml) - .withInsertHandler(OptionsInsertionHandler()) + .withInsertHandler(OptionsInsertionHandler(name)) } private fun createDependsElement(): LookupElement { @@ -130,10 +191,83 @@ private fun createDependsElement(): LookupElement { .withInsertHandler(DependsInsertionHandler()) } -private class OptionsInsertionHandler : InsertHandler { +private fun createEnvStringElement(): LookupElement { + return LookupElementBuilder + .create("") + .withIcon(Icons.LetsYaml) + .withInsertHandler(EnvStringInsertionHandler()) + .withPresentableText("KEY: VALUE (Simple key-value pair)") +} + +private fun createEnvShellElement(): LookupElement { + return LookupElementBuilder + .create("") + .withIcon(Icons.LetsYaml) + .withInsertHandler(EnvShellInsertionHandler()) + .withPresentableText("KEY: sh (Shell script value)") +} + +/** + * Creates template for environment variables with the following structure: + * : + * User must replace `KEY` and `VALUE` with actual values. + */ +private class EnvStringInsertionHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val project = context.project + val editor = context.editor + + // Create a live template + val manager = TemplateManager.getInstance(project) + val template = manager.createTemplate("", "") + template.isToReformat = true + + // Add placeholders + template.addTextSegment("") + template.addVariable("KEY", TextExpression("ENV_KEY"), true) + template.addTextSegment(": ") + template.addVariable("VALUE", TextExpression("ENV_VALUE"), true) + + // Start the template + manager.startTemplate(editor, template) + } +} + +/** + * Creates template for environment variables in shell mode with the following structure: + * : + * sh: + * + * User must replace `KEY` with actual value and write shell script in the next line. + */ +private class EnvShellInsertionHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val padding = "".padStart(4) + + val project = context.project + val editor = context.editor + + // Create a live template + val manager = TemplateManager.getInstance(project) + val template = manager.createTemplate("", "") + template.isToReformat = true + + // Add placeholders + template.addTextSegment("") + template.addVariable("KEY", TextExpression("ENV_KEY"), true) + template.addTextSegment(":\n${padding}sh: ") + + // Start the template + manager.startTemplate(editor, template) + } +} + +private class OptionsInsertionHandler(name: String?) : InsertHandler { + val name = name ?: "" + override fun handleInsert(context: InsertionContext, item: LookupElement) { val padding = "".padStart(COMMAND_CHILD_PADDING) - context.document.insertString(context.selectionEndOffset, ": |\n${padding}Usage: lets ") + context.document.insertString(context.selectionEndOffset, ": |\n${padding}Usage: lets $name") context.editor.caretModel.moveToOffset(context.selectionEndOffset) } } diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt index 24b7039..1e162df 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt @@ -4,6 +4,17 @@ import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.yaml.psi.* + sealed class YamlContextType { + object RootLevel : YamlContextType() + object CommandLevel : YamlContextType() + object ShellLevel : YamlContextType() + object EnvLevel : YamlContextType() + object DependsLevel : YamlContextType() + object RefLevel : YamlContextType() + object Unknown : YamlContextType() +} + + object LetsPsiUtils { fun findCommandsInFile(yamlFile: YAMLFile): List { val commandsKV = PsiTreeUtil.findChildrenOfType(yamlFile, YAMLKeyValue::class.java) @@ -50,11 +61,16 @@ object LetsPsiUtils { /** * Find the command that the given position is in. * If position is inside a command, return the command, otherwise return null. + * + * If contextType is provided, it will be used to determine the context of the position. */ fun findCurrentCommand( - position: PsiElement + position: PsiElement, contextType: YamlContextType? = null ): Command? { val currentKeyValue = PsiTreeUtil.getParentOfType(position, YAMLKeyValue::class.java) ?: return null + if (contextType == YamlContextType.CommandLevel) { + return ConfigParser.parseCommand(currentKeyValue) + } if (!COMMAND_LEVEL_KEYWORDS.contains(currentKeyValue.keyText)) { return null } @@ -85,4 +101,35 @@ object LetsPsiUtils { ?.mapNotNull { it.keyText } ?: emptyList() } + + fun detectContext(position: PsiElement): YamlContextType { + if (isRootLevel(position)) return YamlContextType.RootLevel + + val keyValue = PsiTreeUtil.getParentOfType(position, YAMLKeyValue::class.java) ?: return YamlContextType.Unknown + + return when { + isInCommandKey(keyValue) -> YamlContextType.CommandLevel + isInTopLevelKey(keyValue) -> YamlContextType.RootLevel + keyValue.keyText == "shell" -> YamlContextType.ShellLevel + keyValue.keyText == "env" -> YamlContextType.EnvLevel + keyValue.keyText == "depends" -> YamlContextType.DependsLevel + keyValue.keyText == "ref" -> YamlContextType.RefLevel + else -> YamlContextType.Unknown + } + } + + private fun isRootLevel(position: PsiElement): Boolean { + return ( + position.parent.parent.parent is YAMLFile || + position.parent.parent.parent.parent is YAMLFile + ) + } + private fun isInTopLevelKey(keyValue: YAMLKeyValue): Boolean { + return keyValue.keyText in TOP_LEVEL_KEYWORDS && keyValue.parent?.parent is YAMLFile + } + + private fun isInCommandKey(keyValue: YAMLKeyValue): Boolean { + val parentKeyValue = keyValue.parent?.parent as? YAMLKeyValue ?: return false + return parentKeyValue.keyText == "commands" + } } \ No newline at end of file diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt index 3bf9de7..081834a 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/LetsPsiUtilsTest.kt @@ -154,6 +154,28 @@ class LetsPsiUtilsTest : BasePlatformTestCase() { assertEquals(command.depends.sorted(), listOf("hello").sorted()) } + fun testFindCurrentCommandWithContextType() { + val file = myFixture.configureByText( + "lets.yaml", + """ + shell: bash + commands: + world: + cmd: echo World + depends: [hello] + op + """.trimIndent() + ) + + val element = file.findElementAt(myFixture.caretOffset - 1)!! + var command = LetsPsiUtils.findCurrentCommand(element, YamlContextType.CommandLevel) + assertNotNull(command) + command = command!! + + assertEquals(command.name, "world") + assertEquals(command.depends.sorted(), listOf("hello").sorted()) + } + fun testFindCurrentCommandInDepends() { val file = myFixture.configureByText( "lets.yaml", diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteKeywordTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteKeywordTest.kt index c25d649..f389946 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteKeywordTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/completion/CompleteKeywordTest.kt @@ -80,15 +80,15 @@ open class CompleteKeywordTest : BasePlatformTestCase() { myFixture.completeBasic() - assertEquals(myFixture.caretOffset, 63) + assertEquals(myFixture.caretOffset, 67) assertEquals( """ shell: bash commands: echo: options: | - Usage: lets - """.trimIndent() + " ", + Usage: lets echo + """.trimIndent(), myFixture.file.text.trimIndent(), ) } @@ -107,7 +107,7 @@ open class CompleteKeywordTest : BasePlatformTestCase() { myFixture.completeBasic() - assertEquals(myFixture.caretOffset, 80) + assertEquals(myFixture.caretOffset, 84) assertEquals( """ shell: bash @@ -115,7 +115,7 @@ open class CompleteKeywordTest : BasePlatformTestCase() { echo: cmd: echo Hi options: | - Usage: lets + Usage: lets echo """.trimIndent(), myFixture.file.text.trimIndent(), ) diff --git a/src/test/kotlin/com/github/kindermax/intellijlets/completion/DetectContextTest.kt b/src/test/kotlin/com/github/kindermax/intellijlets/completion/DetectContextTest.kt index 038212f..f6d34b5 100644 --- a/src/test/kotlin/com/github/kindermax/intellijlets/completion/DetectContextTest.kt +++ b/src/test/kotlin/com/github/kindermax/intellijlets/completion/DetectContextTest.kt @@ -1,6 +1,7 @@ package com.github.kindermax.intellijlets.completion -import com.github.kindermax.intellijlets.LetsCompletionHelper +import com.github.kindermax.intellijlets.LetsPsiUtils +import com.github.kindermax.intellijlets.YamlContextType import com.intellij.testFramework.fixtures.BasePlatformTestCase open class DetectContextTest : BasePlatformTestCase() { @@ -19,8 +20,8 @@ open class DetectContextTest : BasePlatformTestCase() { val position = file.findElementAt(myFixture.caretOffset) assertNotNull(position) - val context = LetsCompletionHelper.detectContext(position!!) - assertEquals(context, LetsCompletionHelper.YamlContextType.RootLevel) + val context = LetsPsiUtils.detectContext(position!!) + assertEquals(context, YamlContextType.RootLevel) } fun testCommandLevel() { @@ -38,8 +39,8 @@ open class DetectContextTest : BasePlatformTestCase() { val position = file.findElementAt(offset - 1) assertNotNull("PsiElement should not be null", position) - val context = LetsCompletionHelper.detectContext(position!!) - assertEquals(context, LetsCompletionHelper.YamlContextType.CommandLevel) + val context = LetsPsiUtils.detectContext(position!!) + assertEquals(context, YamlContextType.CommandLevel) } fun testShellLevel() { @@ -53,8 +54,8 @@ open class DetectContextTest : BasePlatformTestCase() { val position = file.findElementAt(offset - 1) assertNotNull("PsiElement should not be null", position) - val context = LetsCompletionHelper.detectContext(position!!) - assertEquals(context, LetsCompletionHelper.YamlContextType.ShellLevel) + val context = LetsPsiUtils.detectContext(position!!) + assertEquals(context, YamlContextType.ShellLevel) } fun testDependsLevel() { @@ -79,8 +80,8 @@ open class DetectContextTest : BasePlatformTestCase() { val position = file.findElementAt(offset) assertNotNull("PsiElement should not be null", position) - val context = LetsCompletionHelper.detectContext(position!!) - assertEquals(context, LetsCompletionHelper.YamlContextType.DependsLevel) + val context = LetsPsiUtils.detectContext(position!!) + assertEquals(context, YamlContextType.DependsLevel) } fun testRefLevel() { @@ -101,7 +102,7 @@ open class DetectContextTest : BasePlatformTestCase() { val position = file.findElementAt(offset - 1) assertNotNull("PsiElement should not be null", position) - val context = LetsCompletionHelper.detectContext(position!!) - assertEquals(context, LetsCompletionHelper.YamlContextType.RefLevel) + val context = LetsPsiUtils.detectContext(position!!) + assertEquals(context, YamlContextType.RefLevel) } } From 8d942d6bfe969b1e2745cf2d67140beb1112e985 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sun, 9 Mar 2025 01:21:34 +0200 Subject: [PATCH 5/5] release 0.0.18 --- CHANGELOG.md | 13 +++++++++++++ gradle.properties | 2 +- .../github/kindermax/intellijlets/LetsPsiUtils.kt | 8 +++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0f8e94..4ee3526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ ## [Unreleased] +### Added + +- Add reference for commands in `ref` +- Add completion for commands in `ref` +- Add completion for env variables in `cmd` scripts from global and command `env` + +### Internal + +- Refactor completions in a way to use LetsPsiUtils +- Drop `Config`, refactor into `ConfigParser +- Drop `LetsCompletionHelper` +- Introduce `YamlContextType` enum instead of boolean functions for determining context type (position type) + ## [0.0.17] - 2025-03-08 ### Added diff --git a/gradle.properties b/gradle.properties index d1023bd..97119da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.kindermax.intellijlets pluginName = intellij-lets pluginRepositoryUrl = https://github.com/lets-cli/intellij-lets -pluginVersion = 0.0.17 +pluginVersion = 0.0.18 pluginSinceBuild = 241 pluginUntilBuild = diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt index 1e162df..daeec25 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsPsiUtils.kt @@ -4,7 +4,7 @@ import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.yaml.psi.* - sealed class YamlContextType { +sealed class YamlContextType { object RootLevel : YamlContextType() object CommandLevel : YamlContextType() object ShellLevel : YamlContextType() @@ -119,11 +119,9 @@ object LetsPsiUtils { } private fun isRootLevel(position: PsiElement): Boolean { - return ( - position.parent.parent.parent is YAMLFile || - position.parent.parent.parent.parent is YAMLFile - ) + return position.parent.parent.parent is YAMLFile || position.parent.parent.parent.parent is YAMLFile } + private fun isInTopLevelKey(keyValue: YAMLKeyValue): Boolean { return keyValue.keyText in TOP_LEVEL_KEYWORDS && keyValue.parent?.parent is YAMLFile }