From c7b16cf80bfafc6c86cdc701702abe7be82636c6 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Fri, 7 Mar 2025 20:06:55 +0200 Subject: [PATCH] complete env vars inside cmd scripts --- example/lets.yaml | 8 +- .../LetsEnvVariableCompletionContributor.kt | 156 ++++++++++++++++++ .../kindermax/intellijlets/constants.kt | 11 ++ src/main/resources/META-INF/plugin.xml | 8 + 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt diff --git a/example/lets.yaml b/example/lets.yaml index 1e0a372..a10d407 100644 --- a/example/lets.yaml +++ b/example/lets.yaml @@ -9,12 +9,18 @@ commands: depends: [build] cmd: echo ${LETS_COMMAND_NAME} - hello: echo Hello + hello: + cmd: echo Hello ${LETS_COMMAND_NAME} and $ + env: + f: 1 + options: | + Usage: lets ${LETS_COMMAND_NAME} run: depends: [test, hello] cmd: | set -ex + $L echo Run echo Rff if [ -n "${LE}" ]; then diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt new file mode 100644 index 0000000..17ddc7a --- /dev/null +++ b/src/main/kotlin/com/github/kindermax/intellijlets/LetsEnvVariableCompletionContributor.kt @@ -0,0 +1,156 @@ +package com.github.kindermax.intellijlets + +import com.intellij.codeInsight.completion.* +import com.intellij.openapi.util.TextRange +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.lang.Language +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.ProcessingContext +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLScalar + +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.psi.PsiFile + +/** + * This class provides completions for environment variables in Lets YAML files. + * Supports: + * - Completion of `$` in `options` key + * - Completion of `$` and `$L` in `cmd` key (only works as a fallback + * if the cmd script is not detected as shell script, see LetsEnvVariableShellScriptCompletionContributor) + */ +class LetsEnvVariableCompletionContributor : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement().inside(YAMLScalar::class.java), + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val element = parameters.position + val caret = parameters.editor.caretModel.currentCaret + val lineOffset = caret.visualLineStart + val prefixText = parameters.editor.document.getText(TextRange(lineOffset, caret.offset)) + + val keyValue = PsiTreeUtil.getParentOfType(element, YAMLKeyValue::class.java) ?: return + + when (keyValue.keyText) { + "options" -> { + val prefixMatcher = result.withPrefixMatcher("$") + prefixMatcher.addElement( + createEnvVariableLookupElement("LETS_COMMAND_NAME") + ) + } + "cmd" -> { + if (prefixText.endsWith("$")) { + val prefixMatcher = result.withPrefixMatcher("$") + BUILTIN_ENV_VARIABLES.forEach { + prefixMatcher.addElement( + createEnvVariableLookupElement(it) + ) + } + } else if (prefixText.endsWith("\$L")) { + val prefixMatcher = result.withPrefixMatcher("\$L") + BUILTIN_ENV_VARIABLES.forEach { + prefixMatcher.addElement( + createEnvVariableLookupElement(it) + ) + } + } + } + } + } + } + ) + } + + override fun beforeCompletion(context: CompletionInitializationContext) { + val offset = context.startOffset + val document = context.editor.document + + // Ensure `$` is treated as a valid trigger for completion + if (offset > 0 && document.charsSequence[offset - 1] == '$') { + context.dummyIdentifier = "$" // This forces completion when `$` is typed + } + } +} + +/** + * This class provides completions for environment variables in Lets YAML files, + * specifically for shell scripts in `cmd` key. + * In order for this completion contributor to work, cmd must be detected as the shell script language. + * If not detected as shell script, the completion will fallback to LetsEnvVariableCompletionContributor. + */ +class LetsEnvVariableShellScriptCompletionContributor : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement().withLanguage(Language.findLanguageByID("Shell Script")!!), + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val element = parameters.position + + // Retrieve the original YAML file from the injected Bash script + val injectedLanguageManager = InjectedLanguageManager.getInstance(element.project) + val yamlFile: PsiFile = injectedLanguageManager.getInjectionHost(element)?.containingFile ?: return + + // Ensure it's a YAML file + if (yamlFile !is org.jetbrains.yaml.psi.YAMLFile) return + + // Retrieve the correct offset in the original YAML file + val hostOffset = injectedLanguageManager.injectedToHost(element, element.textOffset) + + // Find the corresponding element in the original YAML file + val elementAtOffset = yamlFile.findElementAt(hostOffset) ?: return + val yamlKeyValue = PsiTreeUtil.getParentOfType(elementAtOffset, YAMLKeyValue::class.java) ?: return + + // Ensure we are inside `cmd` + if (yamlKeyValue.keyText != "cmd") return + + val prefixText = parameters.editor.document.getText(TextRange(parameters.offset - 1, parameters.offset)) + + if (prefixText.endsWith("$")) { + val prefixMatcher = result.withPrefixMatcher("$") + BUILTIN_ENV_VARIABLES.forEach { + prefixMatcher.addElement( + createEnvVariableLookupElement(it) + ) + } + } else if (prefixText.endsWith("\$L")) { + val prefixMatcher = result.withPrefixMatcher("\$L") + BUILTIN_ENV_VARIABLES.forEach { + prefixMatcher.addElement( + createEnvVariableLookupElement(it) + ) + } + } + } + } + ) + } + + override fun beforeCompletion(context: CompletionInitializationContext) { + val offset = context.startOffset + val document = context.editor.document + + // Ensure `$` is treated as a valid trigger for completion + if (offset > 0 && document.charsSequence[offset - 1] == '$') { + context.dummyIdentifier = "$" // This forces completion when `$` is typed + } + } +} + + +private fun createEnvVariableLookupElement(name: String): LookupElementBuilder { + return LookupElementBuilder.create("\${$name}") + .withPresentableText(name) + .withIcon(Icons.LetsYaml) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/kindermax/intellijlets/constants.kt b/src/main/kotlin/com/github/kindermax/intellijlets/constants.kt index baa409c..ae77b8f 100644 --- a/src/main/kotlin/com/github/kindermax/intellijlets/constants.kt +++ b/src/main/kotlin/com/github/kindermax/intellijlets/constants.kt @@ -34,3 +34,14 @@ val COMMAND_LEVEL_KEYWORDS = listOf( "ref", "args", ) + +val BUILTIN_ENV_VARIABLES = listOf( + "LETS_COMMAND_NAME", + "LETS_COMMAND_ARGS", + "LETS_COMMAND_WORK_DIR", + "LETS_CONFIG", + "LETS_CONFIG_DIR", + "LETS_SHELL", +) + +// TODO: support LETSOPT_ and LETSCLI_ if options is available diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 9e20616..40fa3d4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -27,6 +27,14 @@ order="first" implementationClass="com.github.kindermax.intellijlets.LetsCompletionContributor" /> + +