From f8df55e7074535231554083dc32a22d97af6791c Mon Sep 17 00:00:00 2001 From: Georgii Ustinov Date: Thu, 29 Jan 2026 17:59:14 +0000 Subject: [PATCH] [Java] IDEA-385244 Support javac standard options completion in terminal GitOrigin-RevId: b167cb6dbf75bb5431533356620b9e1d2061f938 --- .../messages/VMOptionsBundle.properties | 46 +++- .../vmOptions/CommandOptionsExtractor.kt | 161 ++++++++++++++ .../execution/vmOptions/VMOptionsParser.kt | 199 +++++++++++++----- .../execution/vmOptions/VMOptionsService.kt | 17 ++ .../vmOptions/VMOptionsServiceImpl.kt | 86 +------- .../resources/intellij.java.terminal.xml | 1 + .../messages/JavaTerminalBundle.properties | 10 +- .../java/terminal/JavaShellCommandContext.kt | 3 +- .../terminal/JavaShellCommandSpecsProvider.kt | 50 ++--- .../java/terminal/JavaShellCommandUtils.kt | 57 +++++ .../JavacShellCommandSpecsProvider.kt | 108 ++++++++++ .../JavaShellCommandSpecsProviderTest.kt | 77 +------ .../JavacShellCommandSpecsProviderTest.kt | 68 ++++++ .../JdkCommandsShellSpecsProviderTestBase.kt | 96 +++++++++ 14 files changed, 733 insertions(+), 246 deletions(-) create mode 100644 java/execution/impl/src/com/intellij/execution/vmOptions/CommandOptionsExtractor.kt create mode 100644 java/java-terminal/src/com/intellij/java/terminal/JavacShellCommandSpecsProvider.kt create mode 100644 java/java-terminal/test/com/intellij/java/terminal/completion/JavacShellCommandSpecsProviderTest.kt create mode 100644 java/java-terminal/test/com/intellij/java/terminal/completion/JdkCommandsShellSpecsProviderTestBase.kt diff --git a/java/execution/impl/resources/messages/VMOptionsBundle.properties b/java/execution/impl/resources/messages/VMOptionsBundle.properties index 5a0326d58454..285cbcbd86ce 100644 --- a/java/execution/impl/resources/messages/VMOptionsBundle.properties +++ b/java/execution/impl/resources/messages/VMOptionsBundle.properties @@ -1,3 +1,4 @@ +# java -X vm.option.enable.assertions.description=Enables assertions with specified granularity. vm.option.enable.system.assertions.description=Enables system assertions. vm.option.disable.assertions.description=Enables assertions with specified granularity. @@ -47,4 +48,47 @@ vm.option.finalization.description=Controls whether the JVM performs finalizatio vm.option.add.exports.description=Updates \\<module\\> to export \\<package\\> to \\<target-module\\>, regardless of module declaration. \\<target-module\\> can be ALL-UNNAMED to export to all unnamed modules. vm.option.source.description=Sets the \\<version\\> of the source in source-file mode. vm.option.disable.files.description=Disables further argument file expansion. -vm.option.illegal.access.description=Permits or denies access to members of types in named modules by code in unnamed modules. \\<value\\> is one of "deny", "permit", "warn", or "debug" This option will be removed in a future release. \ No newline at end of file +vm.option.illegal.access.description=Permits or denies access to members of types in named modules by code in unnamed modules. \\<value\\> is one of "deny", "permit", "warn", or "debug" This option will be removed in a future release. + +# javac -help +vm.option.annotation.processing.description=Specifies options to pass to annotation processors. +vm.option.add.modules.description=Specifies root modules to resolve in addition to the initial modules. +vm.option.boot.classpath.description=Overrides the location of the bootstrap class files. +vm.option.classpath.description=Specifies where to find user class files and annotation processors. +vm.option.directory.description=Sets the destination directory (or class output directory) for class files. +vm.option.deprecation.description=Enables or disables processing of deprecation warnings. +vm.option.enable.preview.description=Enables preview language features. Used in conjunction with either -source or --release. +vm.option.encoding.description=Specifies character encoding used by source files, such as EUC-JP and UTF-8. +vm.option.endorsed.dirs.description=Overrides the location of the endorsed standards path. +vm.option.extension.dirs.description=Overrides the location of the installed extensions. +vm.option.generate.debug.information.description=Generates all debugging information, including local variables. +vm.option.generate.none.debug.information.description=Does not generate debugging information. +vm.option.generate.choice.debug.information.description=Generates only the kinds of debugging information specified by the comma-separated list of keywords. +vm.option.native.header.description=Specifies where to place generated native header files. +vm.option.help.description=Prints a synopsis of the standard options. +vm.option.help.extra.description=Prints a synopsis of the set of extra options. +vm.option.implicit.description=Specifies whether or not to generate class files for implicitly referenced files. +vm.option.j.description=Passes option to the runtime system. +vm.option.limit.modules.javac.description=Limits the universe of observable modules. +vm.option.module.description=Compiles those source files in the named modules that are newer than the corresponding files in the output directory. +vm.option.module.path.description=Specifies where to find application modules. +vm.option.module.source.path.description=Specifies where to find source files when compiling code in multiple modules. +vm.option.module.version.description=Specifies the version of modules that are being compiled. +vm.option.nowarn.description=Disables warning messages (equivalent to -Xlint:none). +vm.option.parameters.description=Generates metadata for reflection on method parameters. +vm.option.proc.description=Controls annotation processing and compilation: none (compilation without annotation processing), only (only annotation processing), or full (annotation processing and compilation). +vm.option.processor.description=Names of the annotation processors to run. +vm.option.processor.module.path.description=Specifies the module path used for finding annotation processors. +vm.option.processor.path.description=Specifies where to find annotation processors. +vm.option.profile.description=Checks that the API used is available in the specified profile. +vm.option.release.description=Compiles source code according to the rules of the Java programming language for a specified Java SE release and generates class files which target that release. +vm.option.source.output.description=Specifies the directory used to place the generated source files. +vm.option.source.release.description=Compiles source code according to the rules of the Java programming language for the specified Java SE release. +vm.option.source.path.description=Specifies where to find source files. +vm.option.system.description=Overrides the location of system modules. +vm.option.target.description=Generates class files suitable for the specified Java SE release. The target release must be equal to or higher than the source release. +vm.option.upgrade.module.path.description=Overrides the location of upgradeable modules. +vm.option.verbose.description=Outputs messages about what the compiler is doing, including information about each class loaded and source file compiled. +vm.option.version.description=Prints version information. +vm.option.werror.description=Terminates compilation when warnings occur. + diff --git a/java/execution/impl/src/com/intellij/execution/vmOptions/CommandOptionsExtractor.kt b/java/execution/impl/src/com/intellij/execution/vmOptions/CommandOptionsExtractor.kt new file mode 100644 index 000000000000..01947ab3044b --- /dev/null +++ b/java/execution/impl/src/com/intellij/execution/vmOptions/CommandOptionsExtractor.kt @@ -0,0 +1,161 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.vmOptions + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessRunner +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.ProcessNotCreatedException +import com.intellij.execution.process.ProcessOutput +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.containers.CollectionFactory +import java.io.File +import java.nio.file.Path +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentMap + +internal abstract class CommandOptionsExtractor { + private val commandData: ConcurrentMap> = + CollectionFactory.createConcurrentSoftValueMap() + + protected abstract val commandName: String + + /** + * Command options used to print all available options + */ + protected abstract val commandOptions: List + + protected abstract fun getOptions(javaHome: String): JdkOptionsData + + fun getOrComputeOptions( + javaHome: String, + ): CompletableFuture { + val future = commandData.computeIfAbsent(javaHome) { CompletableFuture.supplyAsync { getOptions(it) } } + if (future.isDone) { + // sometimes the timeout may appear and in order not to block the possibility to get the completion afterwards, it is better to retry + val data = future.get() + if (data == null) { + commandData.remove(javaHome) + } + } + return future + } + + protected fun getCommandExecutablePath(javaHome: String): String { + val vmExeName = if (SystemInfo.isWindows) "$commandName.exe" else commandName // do not use JavaW.exe because of issues with encoding + return Path.of(getConvertedPath(javaHome), "bin", vmExeName).toString() + } + + private fun getConvertedPath(javaHome: String): String { + // it is copied from com.intellij.openapi.projectRoots.impl.JavaSdkImpl.getConvertedHomePath + var systemDependentName = FileUtil.toSystemDependentName(javaHome) + if (javaHome.endsWith(File.separator)) { + systemDependentName += File.separator + } + return systemDependentName + } + + protected fun opt(name: String, doc: String): VMOption { + return VMOption(name, null, null, VMOptionKind.Standard, doc, VMOptionVariant.DASH, null) + } + + object Java : CommandOptionsExtractor() { + @JvmStatic + @get:JvmName("getStandardOptionList") + val STANDARD_OPTION_LIST: List = listOf( + opt("ea", VMOptionsBundle.message("vm.option.enable.assertions.description")), + opt("enableassertions", VMOptionsBundle.message("vm.option.enable.assertions.description")), + opt("da", VMOptionsBundle.message("vm.option.disable.assertions.description")), + opt("disableassertions", VMOptionsBundle.message("vm.option.disable.assertions.description")), + opt("esa", VMOptionsBundle.message("vm.option.enable.system.assertions.description")), + opt("enablesystemassertions", VMOptionsBundle.message("vm.option.enable.system.assertions.description")), + opt("dsa", VMOptionsBundle.message("vm.option.disable.system.assertions.description")), + opt("disablesystemassertions", VMOptionsBundle.message("vm.option.disable.system.assertions.description")), + opt("agentpath:", VMOptionsBundle.message("vm.option.agentpath.description")), + opt("agentlib:", VMOptionsBundle.message("vm.option.agentlib.description")), + opt("javaagent:", VMOptionsBundle.message("vm.option.javaagent.description")), + opt("D", VMOptionsBundle.message("vm.option.system.property.description")), + opt("XX:", VMOptionsBundle.message("vm.option.advanced.option.description")), + ) + + override val commandName: String = "java" + override val commandOptions: List = + listOf("-XX:+PrintFlagsFinal", "-XX:+UnlockDiagnosticVMOptions", "-XX:+UnlockExperimentalVMOptions", "-X") + + override fun getOptions(javaHome: String): JdkOptionsData { + val options = + getOptionsForJava(javaHome) + return JdkOptionsData(options) + } + + + private fun getOptionsForJava(javaHome: String): List { + val output = getProcessOutput(javaHome) ?: return STANDARD_OPTION_LIST + + val xxOptions = VMOptionsParser.parseXXOptions(output.stdout) + val xOptions = VMOptionsParser.parseXOptions(output.stderr) + if (xOptions != null) { + return xOptions + xxOptions + STANDARD_OPTION_LIST + } + return xxOptions + STANDARD_OPTION_LIST + } + } + + + object Javac : CommandOptionsExtractor() { + val STANDARD_OPTIONS_LIST: List = listOf( + opt("A", "vm.option.annotation.processing.description"), + opt("g", "vm.option.generate.debug.information.description"), + opt("g:", "vm.option.generate.choice.debug.information.description"), + opt("g:none", "vm.option.generate.none.debug.information.description"), + opt("h", "vm.option.native.header.description"), + opt("J", "vm.option.j.description"), + opt("d", "vm.option.directory.description"), + opt("nowarn", "vm.option.nowarn.description"), + opt("parameters", "vm.option.parameters.description"), + opt("processor", "vm.option.processor.description"), + opt("profile", "vm.option.profile.description"), + opt("s", "vm.option.source.output.description"), + opt("verbose", "vm.option.verbose.description"), + opt("Werror", "vm.option.werror.description"), + opt("proc:", "vm.option.proc.description"), + opt("implicit:", "vm.option.implicit.description"), + opt("encoding", "vm.option.encoding.description"), + opt("endorseddirs", "vm.option.endorsed.dirs.description"), + opt("extdirs", "vm.option.extension.dirs.description"), + ) + + override val commandName: String = "javac" + override val commandOptions: List + get() = listOf("-help", "-X") + + override fun getOptions(javaHome: String): JdkOptionsData { + return JdkOptionsData(getOptionsForJavac(javaHome)) + } + + private fun getOptionsForJavac(javaHome: String): List { + val processOutput = getProcessOutput(javaHome) ?: return emptyList() + val parsedStandardOptions = VMOptionsParser.parseJavacDoubleDashedOptions(processOutput.stdout) + return if (parsedStandardOptions != null) parsedStandardOptions + STANDARD_OPTIONS_LIST else STANDARD_OPTIONS_LIST + } + } + + protected fun getProcessOutput(javaHome: String): ProcessOutput? { + val vmPath = getCommandExecutablePath(javaHome) + val generalCommandLine = GeneralCommandLine(vmPath) + generalCommandLine.addParameters(commandOptions) + val handler = try { + OSProcessHandler(generalCommandLine) + } + catch (_: ProcessNotCreatedException) { + return null + } + val runner = CapturingProcessRunner(handler) + val output = runner.runProcess(1000) + if (output.isTimeout) { + return null + } + return output + } +} + diff --git a/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsParser.kt b/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsParser.kt index 4aa2b7ad0fc3..19489a37b55a 100644 --- a/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsParser.kt +++ b/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsParser.kt @@ -43,32 +43,58 @@ internal object VMOptionsParser { } internal fun parseXOptions(stderr: String): List? { - val options = ArrayList() - var currentOption: OptionBuilder? = null var tailIndex = stderr.indexOf("These extra options are subject to change without notice.") if (tailIndex == -1) { tailIndex = stderr.indexOf("The -X options are non-standard and subject to change without notice.") } if (tailIndex == -1) return null val separators = charArrayOf(' ', '<') - for (line in stderr.substring(0, tailIndex).trimStart().lines()) { + + return parseLines(ParsingType.JavaExtraOptions, stderr.substring(0, tailIndex).trimStart().lines(), mapOf("-X" to VMOptionVariant.X, "--" to VMOptionVariant.DASH_DASH), separators) + } + + internal fun parseJavacDoubleDashedOptions(input: String): List? { + val lines = input.lines() + val lastLine = lines.indexOf("") + if (lastLine == -1) return null + val beforeStartLine = lines.indexOf("where possible options include:") + if (beforeStartLine == -1) return null + + return parseLines(ParsingType.JavacStandardOptions, lines.subList(beforeStartLine + 1, lastLine), mapOf("--" to VMOptionVariant.DASH_DASH), charArrayOf(' ')) + } + + private fun parseLines(type: ParsingType, lines: List, allowedOptionsStart: Map, separators: CharArray): List { + val options = ArrayList() + var currentOption: OptionBuilder? = null + for (line in lines) { val trimmed = line.trim() - val variant = when { - trimmed.startsWith("-X") -> VMOptionVariant.X - trimmed.startsWith("--") -> VMOptionVariant.DASH_DASH - else -> null + + var variant : VMOptionVariant? = null + for ((startString, value) in allowedOptionsStart) { + if (trimmed.startsWith(startString)) { + variant = value + break + } } + if (variant != null) { if (currentOption != null) { options.add(currentOption.build()) } val indexOfSeparator = trimmed.indexOfAny(separators) if (indexOfSeparator != -1) { - currentOption = OptionBuilder(variant, trimmed.substring(2, indexOfSeparator)) - currentOption.doc.add(trimmed.substring(indexOfSeparator).trim()) + currentOption = OptionBuilder(type, variant, trimmed.substring(variant.prefix().length, indexOfSeparator)) + val docCandidate = trimmed.substring(indexOfSeparator).trim() + if (docCandidate.startsWith("<")) { + // Option has a format "-opt docs" - documentation starts after second space + currentOption.doc.add(docCandidate) + } else { + // Option has a format "-opt docs" - documentation starts after first space + currentOption.doc.add(docCandidate) + } } else { - currentOption = OptionBuilder(variant, trimmed.substring(2)) + currentOption = OptionBuilder(type,variant, trimmed.substring(variant.prefix().length)) } } else { @@ -82,65 +108,124 @@ internal object VMOptionsParser { return options } - private class OptionBuilder(val variant: VMOptionVariant, name: String) { - val name = name.split("<")[0] + private class OptionBuilder(private val type: ParsingType, val variant: VMOptionVariant, name: String) { + val name = type.parseName(name) val doc = ArrayList() fun build(): VMOption { val vmOptionMapKey = "${variant.prefix()}$name" - val key = VM_OPTION_DESCRIPTION_PROPERTY_KEY_MAP[vmOptionMapKey] + val key = type.optionDescriptionPropertyKey[vmOptionMapKey] val description = if (key != null) { VMOptionsBundle.message(key) } else { LOG.warn("Option $vmOptionMapKey is not localized. Output of java command will be used instead. Please, localize it in VMOptionsBundle") doc.joinToString(separator = " ") } - return VMOption(name, type = null, defaultValue = null, kind = VMOptionKind.Product, doc = description, variant) + return VMOption(name, type = null, defaultValue = null, kind = type.kind, doc = description, variant) } } - private val VM_OPTION_DESCRIPTION_PROPERTY_KEY_MAP: Map = mapOf( - Pair("-Xbatch", "vm.option.batch.description"), - Pair("-Xbootclasspath:", "vm.option.bootclasspath.description"), - Pair("-Xbootclasspath/p:", "vm.option.bootclasspath.p.description"), - Pair("-Xbootclasspath/a:", "vm.option.bootclasspath.a.description"), - Pair("-Xdebug", "vm.option.debug.description"), - Pair("-Xcheck:jni", "vm.option.check.jni.description"), - Pair("-Xcomp", "vm.option.comp.description"), - Pair("-Xdiag", "vm.option.diag.description"), - Pair("-Xfuture", "vm.option.future.description"), - Pair("-Xinternalversion", "vm.option.internalversion.description"), - Pair("-Xlog:", "vm.option.log.description"), - Pair("-Xloggc:", "vm.option.loggc.description"), - Pair("-Xmixed", "vm.option.mixed.description"), - Pair("-Xmn", "vm.option.mn.description"), - Pair("-Xms", "vm.option.ms.description"), - Pair("-Xmx", "vm.option.mx.description"), - Pair("-Xrs", "vm.option.rs.description"), - Pair("-Xnoclassgc", "vm.option.noclassgc.description"), - Pair("-Xshare:auto", "vm.option.share.auto.description"), - Pair("-Xshare:off", "vm.option.share.off.description"), - Pair("-Xshare:on", "vm.option.share.on.description"), - Pair("-XshowSettings", "vm.option.showSettings.description"), - Pair("-XshowSettings:all", "vm.option.showSettings.all.description"), - Pair("-XshowSettings:locale", "vm.option.showSettings.locale.description"), - Pair("-XshowSettings:properties", "vm.option.showSettings.properties.description"), - Pair("-XshowSettings:vm", "vm.option.showSettings.vm.description"), - Pair("-XshowSettings:system", "vm.option.showSettings.system.description"), - Pair("-Xss", "vm.option.ss.description"), - Pair("-Xverify", "vm.option.verify.description"), - Pair("-Xincgc", "vm.option.incgc.description"), - Pair("-Xprof", "vm.option.prof.description"), - Pair("-Xint", "vm.option.int.description"), - Pair("--add-reads", "vm.option.add.reads.description"), - Pair("--add-opens", "vm.option.add.opens.description"), - Pair("--limit-modules", "vm.option.limit.modules.description"), - Pair("--patch-module", "vm.option.patch.module.description"), - Pair("--finalization=", "vm.option.finalization.description"), - Pair("--add-exports", "vm.option.add.exports.description"), - Pair("--source", "vm.option.source.description"), - Pair("--disable-@files", "vm.option.disable.files.description"), - Pair("--illegal-access=", "vm.option.illegal.access.description") - ) + private interface ParsingType { + val optionDescriptionPropertyKey: Map + + val kind: VMOptionKind + + fun parseName(input: String): String + + object JavaExtraOptions : ParsingType { + override val optionDescriptionPropertyKey: Map = mapOf( + Pair("-Xbatch", "vm.option.batch.description"), + Pair("-Xbootclasspath:", "vm.option.bootclasspath.description"), + Pair("-Xbootclasspath/p:", "vm.option.bootclasspath.p.description"), + Pair("-Xbootclasspath/a:", "vm.option.bootclasspath.a.description"), + Pair("-Xdebug", "vm.option.debug.description"), + Pair("-Xcheck:jni", "vm.option.check.jni.description"), + Pair("-Xcomp", "vm.option.comp.description"), + Pair("-Xdiag", "vm.option.diag.description"), + Pair("-Xfuture", "vm.option.future.description"), + Pair("-Xinternalversion", "vm.option.internalversion.description"), + Pair("-Xlog:", "vm.option.log.description"), + Pair("-Xloggc:", "vm.option.loggc.description"), + Pair("-Xmixed", "vm.option.mixed.description"), + Pair("-Xmn", "vm.option.mn.description"), + Pair("-Xms", "vm.option.ms.description"), + Pair("-Xmx", "vm.option.mx.description"), + Pair("-Xrs", "vm.option.rs.description"), + Pair("-Xnoclassgc", "vm.option.noclassgc.description"), + Pair("-Xshare:auto", "vm.option.share.auto.description"), + Pair("-Xshare:off", "vm.option.share.off.description"), + Pair("-Xshare:on", "vm.option.share.on.description"), + Pair("-XshowSettings", "vm.option.showSettings.description"), + Pair("-XshowSettings:all", "vm.option.showSettings.all.description"), + Pair("-XshowSettings:locale", "vm.option.showSettings.locale.description"), + Pair("-XshowSettings:properties", "vm.option.showSettings.properties.description"), + Pair("-XshowSettings:vm", "vm.option.showSettings.vm.description"), + Pair("-XshowSettings:system", "vm.option.showSettings.system.description"), + Pair("-Xss", "vm.option.ss.description"), + Pair("-Xverify", "vm.option.verify.description"), + Pair("-Xincgc", "vm.option.incgc.description"), + Pair("-Xprof", "vm.option.prof.description"), + Pair("-Xint", "vm.option.int.description"), + Pair("--add-reads", "vm.option.add.reads.description"), + Pair("--add-opens", "vm.option.add.opens.description"), + Pair("--limit-modules", "vm.option.limit.modules.description"), + Pair("--patch-module", "vm.option.patch.module.description"), + Pair("--finalization=", "vm.option.finalization.description"), + Pair("--add-exports", "vm.option.add.exports.description"), + Pair("--source", "vm.option.source.description"), + Pair("--disable-@files", "vm.option.disable.files.description"), + Pair("--illegal-access=", "vm.option.illegal.access.description") + ) + + override val kind: VMOptionKind = VMOptionKind.Product + + override fun parseName(input: String): String { + return input.split("<").first() + } + } + + object JavacStandardOptions : ParsingType { + override val optionDescriptionPropertyKey: Map = mapOf( + Pair("--add-modules", "vm.option.add.modules.description"), + Pair("--boot-class-path", "vm.option.boot.classpath.description"), + Pair("--class-path", "vm.option.classpath.description"), + Pair("-deprecation", "vm.option.deprecation.description"), + Pair("--enable-preview", "vm.option.enable.preview.description"), + Pair("--help,", "vm.option.help.description"), + Pair("--help-extra,", "vm.option.help.extra.description"), + Pair("--limit-modules", "vm.option.limit.modules.javac.description"), + Pair("--module", "vm.option.module.description"), + Pair("--module-path", "vm.option.module.path.description"), + Pair("--module-source-path", "vm.option.module.source.path.description"), + Pair("--module-version", "vm.option.module.version.description"), + Pair("--processor-module-path", "vm.option.processor.module.path.description"), + Pair("--processor-path", "vm.option.processor.path.description"), + Pair("--release", "vm.option.release.description"), + Pair("--source", "vm.option.source.release.description"), + Pair("--source-path", "vm.option.source.path.description"), + Pair("--system", "vm.option.system.description"), + Pair("--target", "vm.option.target.description"), + Pair("--upgrade-module-path", "vm.option.upgrade.module.path.description"), + Pair("--version,", "vm.option.version.description"), + ) + + override val kind: VMOptionKind = VMOptionKind.Standard + + override fun parseName(input: String): String { + // parsing names "-opt:{arguments}" + input.extractPrefix(":{", ":")?.let { return it } + + // parsing names "-opt[=value]" + input.extractPrefix("[", "")?.let { return it } + + return input.extractPrefix("<", shouldHaveTwoParts = false) ?: throw IllegalArgumentException("Cannot parse option name: $input") + } + + private fun String.extractPrefix(delimiter: String, suffix: String = "", shouldHaveTwoParts: Boolean = true): String? { + val parts = split(delimiter) + return if (parts.size == 2 || !shouldHaveTwoParts) parts.first().trimEnd(',') else null + } + } + } } \ No newline at end of file diff --git a/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsService.kt b/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsService.kt index cbc39476b368..815561ceb430 100644 --- a/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsService.kt +++ b/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsService.kt @@ -10,7 +10,24 @@ interface VMOptionsService { fun getInstance(): VMOptionsService = service() } + /** + * Calculates all the options for the `java` command. + */ fun getOrComputeOptionsForJdk(javaHome: String): CompletableFuture + /** + * Calculates all the options for the `javac` command. + */ + fun getOrComputeOptionsForJavac(javaHome: String): CompletableFuture = + CompletableFuture.completedFuture(JdkOptionsData(emptyList())) + + /** + * @return options that appears in all `java` commands starting from java 8 except informational (help, version) + */ fun getStandardOptions(): JdkOptionsData = JdkOptionsData(emptyList()) + + /** + * @return options that appears in all `javac` commands starting from java 8 except informational (help, version) + */ + fun getStandardJavacOptions(): JdkOptionsData = JdkOptionsData(emptyList()) } \ No newline at end of file diff --git a/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsServiceImpl.kt b/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsServiceImpl.kt index 89feffce8503..7def551356a5 100644 --- a/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsServiceImpl.kt +++ b/java/execution/impl/src/com/intellij/execution/vmOptions/VMOptionsServiceImpl.kt @@ -1,98 +1,24 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.vmOptions -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.process.CapturingProcessRunner -import com.intellij.execution.process.OSProcessHandler -import com.intellij.execution.process.ProcessNotCreatedException -import com.intellij.openapi.util.SystemInfo -import com.intellij.openapi.util.io.FileUtil -import com.intellij.util.containers.CollectionFactory -import java.io.File -import java.nio.file.Path import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentMap class VMOptionsServiceImpl : VMOptionsService { companion object { - private val ourData: ConcurrentMap> = CollectionFactory.createConcurrentSoftValueMap() - @JvmStatic @get:JvmName("getStandardOptionList") - val STANDARD_OPTION_LIST : List = listOf( - opt("ea", VMOptionsBundle.message("vm.option.enable.assertions.description")), - opt("enableassertions", VMOptionsBundle.message("vm.option.enable.assertions.description")), - opt("da", VMOptionsBundle.message("vm.option.disable.assertions.description")), - opt("disableassertions", VMOptionsBundle.message("vm.option.disable.assertions.description")), - opt("esa", VMOptionsBundle.message("vm.option.enable.system.assertions.description")), - opt("enablesystemassertions", VMOptionsBundle.message("vm.option.enable.system.assertions.description")), - opt("dsa", VMOptionsBundle.message("vm.option.disable.system.assertions.description")), - opt("disablesystemassertions", VMOptionsBundle.message("vm.option.disable.system.assertions.description")), - opt("agentpath:",VMOptionsBundle.message("vm.option.agentpath.description")), - opt("agentlib:", VMOptionsBundle.message("vm.option.agentlib.description")), - opt("javaagent:", VMOptionsBundle.message("vm.option.javaagent.description")), - opt("D", VMOptionsBundle.message("vm.option.system.property.description")), - opt("XX:", VMOptionsBundle.message("vm.option.advanced.option.description")), - ) - - private fun opt(name: String, doc: String): VMOption { - return VMOption(name, null, null, VMOptionKind.Standard, doc, VMOptionVariant.DASH, null) - } + val STANDARD_OPTION_LIST : List = CommandOptionsExtractor.Java.STANDARD_OPTION_LIST } override fun getOrComputeOptionsForJdk(javaHome: String): CompletableFuture { - val future = ourData.computeIfAbsent(javaHome) { CompletableFuture.supplyAsync { computeOptionsData(it) } } - if (future.isDone) { - // sometimes the timeout may appear and in order not to block the possibility to get the completion afterwards, it is better to retry - val data = future.get() - if (data == null) { - ourData.remove(javaHome) - } - } - return future + return CommandOptionsExtractor.Java.getOrComputeOptions(javaHome) } - override fun getStandardOptions(): JdkOptionsData = JdkOptionsData(STANDARD_OPTION_LIST) - - // when null is returned, it was a timeout - private fun computeOptionsData(javaHome: String): JdkOptionsData { - return JdkOptionsData(getOptionsForJdk(javaHome)) + override fun getOrComputeOptionsForJavac(javaHome: String): CompletableFuture { + return CommandOptionsExtractor.Javac.getOrComputeOptions(javaHome) } - private fun getOptionsForJdk(javaHome: String): List { - val vmPath = getVmPath(javaHome) - val generalCommandLine = GeneralCommandLine(vmPath) - generalCommandLine.addParameters("-XX:+PrintFlagsFinal", "-XX:+UnlockDiagnosticVMOptions", "-XX:+UnlockExperimentalVMOptions", "-X") - val handler = try { - OSProcessHandler(generalCommandLine) - } - catch (e: ProcessNotCreatedException) { - return STANDARD_OPTION_LIST - } - val runner = CapturingProcessRunner(handler) - val output = runner.runProcess(1000) - if (output.isTimeout) { - return STANDARD_OPTION_LIST - } - val xxOptions = VMOptionsParser.parseXXOptions(output.stdout) - val xOptions = VMOptionsParser.parseXOptions(output.stderr) - if (xOptions != null) { - return xOptions + xxOptions + STANDARD_OPTION_LIST - } - return xxOptions + STANDARD_OPTION_LIST - } + override fun getStandardOptions(): JdkOptionsData = JdkOptionsData(CommandOptionsExtractor.Java.STANDARD_OPTION_LIST) - private fun getVmPath(javaHome: String): String { - val vmExeName = if (SystemInfo.isWindows) "java.exe" else "java" // do not use JavaW.exe because of issues with encoding - return Path.of(getConvertedPath(javaHome), "bin", vmExeName).toString() - } - - private fun getConvertedPath(javaHome: String): String { - // it is copied from com.intellij.openapi.projectRoots.impl.JavaSdkImpl.getConvertedHomePath - var systemDependentName = FileUtil.toSystemDependentName(javaHome) - if (javaHome.endsWith(File.separator)) { - systemDependentName += File.separator - } - return systemDependentName - } + override fun getStandardJavacOptions(): JdkOptionsData = JdkOptionsData(CommandOptionsExtractor.Javac.STANDARD_OPTIONS_LIST) } \ No newline at end of file diff --git a/java/java-terminal/resources/intellij.java.terminal.xml b/java/java-terminal/resources/intellij.java.terminal.xml index 96e04606aa5a..353b14db12e4 100644 --- a/java/java-terminal/resources/intellij.java.terminal.xml +++ b/java/java-terminal/resources/intellij.java.terminal.xml @@ -8,6 +8,7 @@ + diff --git a/java/java-terminal/resources/messages/JavaTerminalBundle.properties b/java/java-terminal/resources/messages/JavaTerminalBundle.properties index 738879f07877..8317c2eb8592 100644 --- a/java/java-terminal/resources/messages/JavaTerminalBundle.properties +++ b/java/java-terminal/resources/messages/JavaTerminalBundle.properties @@ -1,3 +1,4 @@ +# "java" command java.command.terminal.description=Runs java. java.command.terminal.help.option.description=Prints the help message to the {0} stream. java.command.terminal.jar.option.description=Launches the main class in a JAR file. @@ -9,4 +10,11 @@ java.command.terminal.enable.preview.option.description=Allows classes to depend java.command.terminal.verbose.option.description=Enables verbose output for the given subsystem. external.java.configuration.run.command=Run ''{0}''\u2026 error.stream.name=error -output.stream.name=output \ No newline at end of file +output.stream.name=output + +# "javac" command +java.c.command.terminal.description=Reads Java declarations and compile them into class files. +java.c.command.terminal.classpath.option.description=Specifies where to find user class files, and (optionally) annotation processors and source files. +java.c.command.terminal.annotation.processing.option.description=Specifies options to pass to annotation processors. +java.c.command.terminal.extra.options.command.description=Prints the help for extra options. + diff --git a/java/java-terminal/src/com/intellij/java/terminal/JavaShellCommandContext.kt b/java/java-terminal/src/com/intellij/java/terminal/JavaShellCommandContext.kt index d8b9270d705a..162049834398 100644 --- a/java/java-terminal/src/com/intellij/java/terminal/JavaShellCommandContext.kt +++ b/java/java-terminal/src/com/intellij/java/terminal/JavaShellCommandContext.kt @@ -15,10 +15,9 @@ class JavaShellCommandContext private constructor(private val propertyMap: Map) { + jdkOptionsData.options + .filter { (it.kind == VMOptionKind.Standard || it.kind == VMOptionKind.Product) && (it.variant != VMOptionVariant.XX)} + .toList() + .forEach { + val optionName = it.optionName + val presentableName = "${it.variant.prefix()}$optionName" + option(presentableName) { + val optionDescription = it.doc + if (optionDescription != null) { + description(optionDescription) + } + + val info = uiMap[presentableName] ?: DEFAULT_UI_OPTION_INSTANCE + repeatTimes(info.repeatTimes) + info.separator?.let { separator(it) } + val argumentName = info.argumentName + if (argumentName != null) { + argument { + if (info.isArgumentOptional) optional() + displayName(argumentName) + + if (info.suggestionsGenerator != null) suggestions(info.suggestionsGenerator) + } + } + } + } + } + + internal fun ShellCommandContext.addClassPathOption() { + option("-classpath", "-cp") { + exclusiveOn(listOf("--class-path")) + description(JavaTerminalBundle.message("java.command.terminal.classpath.option.description")) + argument { + suggestions(classpathSuggestionsGenerator()) + displayName(CLASSPATH_ARGUMENT_NAME) + } + } + } + + internal val CLASSPATH_ARGUMENT_NAME: @NlsSafe String = "filepath[${getClassPathSeparator()}filepath]" + fun getClassPathSeparator(): String = when { SystemInfo.isWindows -> ";" else -> ":" @@ -42,5 +91,13 @@ object JavaShellCommandUtils { } } + internal data class UIOptionInfo( + val separator: String? = null, + @NlsSafe val argumentName: String? = null, + val repeatTimes: Int = 1, + val isArgumentOptional: Boolean = false, + val suggestionsGenerator: ShellRuntimeDataGenerator>? = null, + ) + private data class PathInfo(val typedPrefix: String, val replacementIndexDelta: Int = 0) } \ No newline at end of file diff --git a/java/java-terminal/src/com/intellij/java/terminal/JavacShellCommandSpecsProvider.kt b/java/java-terminal/src/com/intellij/java/terminal/JavacShellCommandSpecsProvider.kt new file mode 100644 index 000000000000..886189f0092e --- /dev/null +++ b/java/java-terminal/src/com/intellij/java/terminal/JavacShellCommandSpecsProvider.kt @@ -0,0 +1,108 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.terminal + +import com.intellij.execution.vmOptions.VMOptionsService +import com.intellij.java.terminal.JavaShellCommandUtils.UIOptionInfo +import com.intellij.java.terminal.JavaShellCommandUtils.addClassPathOption +import com.intellij.java.terminal.JavaShellCommandUtils.addOptionsFromData +import com.intellij.java.terminal.JavaShellCommandUtils.classpathSuggestionsGenerator +import com.intellij.openapi.util.NlsSafe +import com.intellij.terminal.completion.spec.ShellCommandParserOptions +import com.intellij.terminal.completion.spec.ShellCommandSpec +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.Nls +import org.jetbrains.plugins.terminal.block.completion.spec.* +import org.jetbrains.plugins.terminal.block.completion.spec.dsl.ShellChildOptionsContext + +internal class JavacShellCommandSpecsProvider : ShellCommandSpecsProvider { + override fun getCommandSpecs(): List { + return listOf(ShellCommandSpecInfo.create(getSpecs(), ShellCommandSpecConflictStrategy.REPLACE)) + } + + private fun getSpecs(): ShellCommandSpec = ShellCommandSpec("javac") { + parserOptions( + ShellCommandParserOptions.builder() + .flagsArePosixNonCompliant(true) + .optionsMustPrecedeArguments(true) + .build() + ) + + description(JavaTerminalBundle.message("java.c.command.terminal.description")) + + dynamicOptions { terminalContext -> + val javaContext = JavaShellCommandContext.create(terminalContext) + val path = javaContext?.getJrePath() + addJavacOptions(path) + } + + + val streamName: @Nls String = JavaTerminalBundle.message("output.stream.name") + + option("-?", "-help") { + exclusiveOn(listOf("--help")) + description(JavaTerminalBundle.message("java.command.terminal.help.option.description", streamName)) + } + + option("-X") { + exclusiveOn(listOf("--help-extra")) + description(JavaTerminalBundle.message("java.c.command.terminal.extra.options.command.description")) + } + + option("-version") { + exclusiveOn(listOf("--version")) + description(JavaTerminalBundle.message("java.command.terminal.version.option.description", streamName)) + } + + addClassPathOption() + + argument { + displayName(SOURCE_FILE_ARGUMENT_NAME) + suggestions(ShellDataGenerators.fileSuggestionsGenerator()) + } + } + + private suspend fun ShellChildOptionsContext.addJavacOptions(path: String?) { + val optionsService = VMOptionsService.getInstance() + val jdkOptionsData = withContext(Dispatchers.IO) { + + if (path == null) return@withContext null + optionsService.getOrComputeOptionsForJavac(path).get() + } ?: optionsService.getStandardJavacOptions() + + addOptionsFromData(jdkOptionsData, UI_OPTION_INFO_MAP) + } +} + +private const val SOURCE_FILE_ARGUMENT_NAME: @NlsSafe String = "[sourcefiles-or-classnames]" +private val UI_OPTION_INFO_MAP = mapOf( + "-A" to UIOptionInfo(separator = "", argumentName = "key[=value]"), + "--add-modules" to UIOptionInfo(argumentName = "module[,module]*"), + "--boot-class-path" to UIOptionInfo(argumentName = "path", suggestionsGenerator = classpathSuggestionsGenerator()), + "--class-path" to UIOptionInfo(argumentName = "path", suggestionsGenerator = classpathSuggestionsGenerator()), + "-d" to UIOptionInfo(argumentName = "directory", suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator(true)), + "-encoding" to UIOptionInfo(argumentName = "encoding"), + "-endorseddirs" to UIOptionInfo(argumentName = "dirs"), + "-extdirs" to UIOptionInfo(argumentName = "directories"), + "-g:" to UIOptionInfo(separator = "", argumentName = "none|lines|vars"), + "-h" to UIOptionInfo(argumentName = "directory", suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator(true)), + "-implicit:" to UIOptionInfo(separator = "", argumentName = "none|class", isArgumentOptional = true), + "-J" to UIOptionInfo(separator = "", argumentName = "option"), + "--limit-modules" to UIOptionInfo(argumentName = "module[,module]*"), + "--module" to UIOptionInfo(argumentName = "module-name"), + "--module-path" to UIOptionInfo(argumentName = "path"), + "--module-source-path" to UIOptionInfo(argumentName = "path"), + "--module-version" to UIOptionInfo(argumentName = "version"), + "-proc:" to UIOptionInfo(separator = "", argumentName = "none|only|full", isArgumentOptional = true), + "-processor" to UIOptionInfo(argumentName = "class[,class]*"), + "--processor-module-path" to UIOptionInfo(argumentName = "path", suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator()), + "--processor-path" to UIOptionInfo(argumentName = "path" , suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator()), + "-profile" to UIOptionInfo(argumentName = "profile"), + "--release" to UIOptionInfo(argumentName = "release"), + "-s" to UIOptionInfo(argumentName = "directory", suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator(true)), + "--source" to UIOptionInfo(argumentName = "release"), + "--source-path" to UIOptionInfo(argumentName = "path", suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator()), + "--system" to UIOptionInfo(argumentName = "jdk"), + "--target" to UIOptionInfo(argumentName = "release"), + "--upgrade-module-path" to UIOptionInfo(argumentName = "path", suggestionsGenerator = ShellDataGenerators.fileSuggestionsGenerator()), +) \ No newline at end of file diff --git a/java/java-terminal/test/com/intellij/java/terminal/completion/JavaShellCommandSpecsProviderTest.kt b/java/java-terminal/test/com/intellij/java/terminal/completion/JavaShellCommandSpecsProviderTest.kt index bd1d15dd40fc..8138fe3f6caf 100644 --- a/java/java-terminal/test/com/intellij/java/terminal/completion/JavaShellCommandSpecsProviderTest.kt +++ b/java/java-terminal/test/com/intellij/java/terminal/completion/JavaShellCommandSpecsProviderTest.kt @@ -1,28 +1,20 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.java.terminal.completion -import com.intellij.execution.vmOptions.* -import com.intellij.java.terminal.JavaShellCommandContext import com.intellij.java.terminal.JavaShellCommandUtils -import com.intellij.openapi.application.ApplicationManager import com.intellij.terminal.completion.spec.ShellCommandResult -import com.intellij.terminal.completion.spec.ShellCompletionSuggestion -import com.intellij.terminal.completion.spec.ShellFileInfo import com.intellij.testFramework.UsefulTestCase -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import com.intellij.testFramework.replaceService -import junit.framework.TestCase import kotlinx.coroutines.runBlocking import org.jetbrains.plugins.terminal.TerminalEngine -import org.jetbrains.plugins.terminal.block.completion.spec.ShellFileSystemSupport +import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGeneratorProcessExecutor +import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGeneratorProcessOptions import org.jetbrains.plugins.terminal.testFramework.completion.ShellCompletionTestFixture import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import java.util.concurrent.CompletableFuture @RunWith(Parameterized::class) -class JavaShellCommandSpecsProviderTest(private val engine: TerminalEngine) : BasePlatformTestCase() { +internal class JavaShellCommandSpecsProviderTest(engine: TerminalEngine) : JdkCommandsShellSpecsProviderTestBase(engine) { companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") @@ -31,7 +23,13 @@ class JavaShellCommandSpecsProviderTest(private val engine: TerminalEngine) : Ba @Test fun `default options are present`() = runBlocking { - val fixture = ShellCompletionTestFixture.builder(project).mockShellCommandResults { _ -> + val fixture = ShellCompletionTestFixture.builder(project) + .mockProcessesExecutor(object : ShellDataGeneratorProcessExecutor { + override suspend fun executeProcess(options: ShellDataGeneratorProcessOptions): ShellCommandResult { + return ShellCommandResult.create("", exitCode = 1) + } + }) + .mockShellCommandResults { _ -> return@mockShellCommandResults ShellCommandResult.create("", exitCode = 1) }.build() assertSameElements(fixture.getCompletionNames(), listOf("-ea", "-enableassertions", "-da", "-disableassertions", "-esa", "-enablesystemassertions", @@ -101,7 +99,6 @@ class JavaShellCommandSpecsProviderTest(private val engine: TerminalEngine) : Ba assertTrue(completion.all { it.prefixReplacementIndex == argument.length - 1 }) } - @Test fun `classpath suggestion generator simple`() = runBlocking { val fixture = createFixture() @@ -129,58 +126,4 @@ class JavaShellCommandSpecsProviderTest(private val engine: TerminalEngine) : Ba assertSameElements(completion.map { it.name }, listOf("file1.jar", "file2.jar", "dir1/")) assertTrue(completion.all { it.prefixReplacementIndex == argument.length}) } - - private fun createFixture(javaVersion: Int = 11): ShellCompletionTestFixture { - ApplicationManager.getApplication().replaceService(VMOptionsService::class.java, MockVMOptionsService(), testRootDisposable) - val fixture = ShellCompletionTestFixture.builder(project) - .setIsReworkedTerminal(engine == TerminalEngine.REWORKED) - .mockShellCommandResults { command -> - if (command == JavaShellCommandContext.JAVA_SHOW_SETTINGS_PROPERTIES_VERSION_COMMAND) { - return@mockShellCommandResults ShellCommandResult.create("java.home = /jre/home\njava.version = ${javaVersion}", exitCode = 0) - } - - if (command.startsWith("__jetbrains_intellij_get_directory_files")) { - return@mockShellCommandResults ShellCommandResult.create("file1.jar\nfile2.jar\ndir1/", exitCode = 0) - } - - return@mockShellCommandResults ShellCommandResult.create("", exitCode = 1) - } - .mockFileSystemSupport(object : ShellFileSystemSupport { - override suspend fun listDirectoryFiles(path: String): List { - return listOf( - ShellFileInfo.create("file1.jar", ShellFileInfo.Type.FILE), - ShellFileInfo.create("file2.jar", ShellFileInfo.Type.FILE), - ShellFileInfo.create("dir1", ShellFileInfo.Type.DIRECTORY), - ) - } - }) - .build() - return fixture - } - - private suspend fun ShellCompletionTestFixture.getCompletionNames(command: String = "java "): List { - val actual: List = getCompletions(command) - return actual.map { it.name } - } - - private class MockVMOptionsService : VMOptionsService { - override fun getOrComputeOptionsForJdk(javaHome: String): CompletableFuture { - TestCase.assertEquals("/jre/home", javaHome) - return CompletableFuture.completedFuture( - JdkOptionsData( - listOf( - VMOption("settings", null, null, VMOptionKind.Product, null, VMOptionVariant.X), - VMOption("lint", null, null, VMOptionKind.Standard, null, VMOptionVariant.X), - VMOption("experiment", null, null, VMOptionKind.Experimental, null, VMOptionVariant.X), - VMOption("diagnose", null, null, VMOptionKind.Diagnostic, null, VMOptionVariant.X), - VMOption("advanced", null, null, VMOptionKind.Product, null, VMOptionVariant.XX), - VMOption("add-opens", null, null, VMOptionKind.Product, null, VMOptionVariant.DASH_DASH), - VMOption("add-exports", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), - VMOption("add-experiment-exports", null, null, VMOptionKind.Experimental, null, VMOptionVariant.DASH_DASH), - VMOption("add-diagnostic-exports", null, null, VMOptionKind.Experimental, null, VMOptionVariant.DASH_DASH), - ) - ) - ) - } - } } \ No newline at end of file diff --git a/java/java-terminal/test/com/intellij/java/terminal/completion/JavacShellCommandSpecsProviderTest.kt b/java/java-terminal/test/com/intellij/java/terminal/completion/JavacShellCommandSpecsProviderTest.kt new file mode 100644 index 000000000000..3e78d252b109 --- /dev/null +++ b/java/java-terminal/test/com/intellij/java/terminal/completion/JavacShellCommandSpecsProviderTest.kt @@ -0,0 +1,68 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.terminal.completion + +import com.intellij.java.terminal.JavaShellCommandUtils +import com.intellij.terminal.completion.spec.ShellCommandResult +import com.intellij.testFramework.UsefulTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.plugins.terminal.TerminalEngine +import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGeneratorProcessExecutor +import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGeneratorProcessOptions +import org.jetbrains.plugins.terminal.testFramework.completion.ShellCompletionTestFixture +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +internal class JavacShellCommandSpecsProviderTest(engine: TerminalEngine) : JdkCommandsShellSpecsProviderTestBase(engine) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun engine(): List = listOf(TerminalEngine.REWORKED, TerminalEngine.NEW_TERMINAL) + } + + @Test + fun `default options are present`() = runBlocking { + val fixture = ShellCompletionTestFixture.builder(project) + .mockProcessesExecutor(object : ShellDataGeneratorProcessExecutor { + override suspend fun executeProcess(options: ShellDataGeneratorProcessOptions): ShellCommandResult { + return ShellCommandResult.create("", exitCode = 1) + } + }) + .mockShellCommandResults { _ -> + return@mockShellCommandResults ShellCommandResult.create("", exitCode = 1) + }.build() + assertSameElements(fixture.getCompletionNames("javac "), listOf( + "-A", "-g", "-g:", "-g:none", "-h", "-J", "-d", "-nowarn", "-parameters", "-processor", + "-profile", "-s", "-verbose", "-Werror", "-proc:", "-implicit:", + "-encoding", "-endorseddirs", "-extdirs", "-cp", "-classpath", "-?", "-help", "-X", "-version")) + } + + @Test + fun `double dash options are present`() = runBlocking { + val fixture = createFixture() + UsefulTestCase.assertContainsElements(fixture.getCompletionNames("javac "), listOf("--enable-preview", "--release", "--source", "--target")) + } + + @Test + fun `classpath suggestion generator`() = runBlocking { + val separator = JavaShellCommandUtils.getClassPathSeparator() + val fixture = createFixture() + val argument = "file1.jar$separator" + val completion = fixture.getCompletions("javac -cp $argument") + assertSameElements(completion.map { it.name }, listOf("file1.jar", "file2.jar", "dir1/")) + assertTrue(completion.all { it.prefixReplacementIndex == argument.length }) + } + + @Test + fun `boot classpath suggestion generator`() = runBlocking { + val separator = JavaShellCommandUtils.getClassPathSeparator() + val fixture = createFixture() + val argument = "file1.jar$separator" + val completion = fixture.getCompletions("javac --boot-class-path $argument") + assertSameElements(completion.map { it.name }, listOf("file1.jar", "file2.jar", "dir1/")) + assertTrue(completion.all { it.prefixReplacementIndex == argument.length }) + } +} + + diff --git a/java/java-terminal/test/com/intellij/java/terminal/completion/JdkCommandsShellSpecsProviderTestBase.kt b/java/java-terminal/test/com/intellij/java/terminal/completion/JdkCommandsShellSpecsProviderTestBase.kt new file mode 100644 index 000000000000..dee425536f8e --- /dev/null +++ b/java/java-terminal/test/com/intellij/java/terminal/completion/JdkCommandsShellSpecsProviderTestBase.kt @@ -0,0 +1,96 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.terminal.completion + +import com.intellij.execution.vmOptions.* +import com.intellij.openapi.application.ApplicationManager +import com.intellij.terminal.completion.spec.ShellCommandResult +import com.intellij.terminal.completion.spec.ShellCompletionSuggestion +import com.intellij.terminal.completion.spec.ShellFileInfo +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.replaceService +import junit.framework.TestCase +import org.jetbrains.plugins.terminal.TerminalEngine +import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGeneratorProcessExecutor +import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGeneratorProcessOptions +import org.jetbrains.plugins.terminal.block.completion.spec.ShellFileSystemSupport +import org.jetbrains.plugins.terminal.testFramework.completion.ShellCompletionTestFixture +import java.util.concurrent.CompletableFuture + +internal abstract class JdkCommandsShellSpecsProviderTestBase(private val engine: TerminalEngine) : BasePlatformTestCase() { + protected fun createFixture(javaVersion: Int = 11): ShellCompletionTestFixture { + ApplicationManager.getApplication().replaceService(VMOptionsService::class.java, MockVMOptionsService(), testRootDisposable) + val fixture = ShellCompletionTestFixture.builder(project) + .setIsReworkedTerminal(engine == TerminalEngine.REWORKED) + .mockProcessesExecutor(object : ShellDataGeneratorProcessExecutor { + override suspend fun executeProcess(options: ShellDataGeneratorProcessOptions): ShellCommandResult { + if (options.executable == "java" && options.args == listOf("-XshowSettings:properties", "-version")) { + return ShellCommandResult.create("java.home = /jre/home\njava.version = $javaVersion", exitCode = 0) + } + else if (options.executable == "__jetbrains_intellij_get_directory_files") { + return ShellCommandResult.create("file1.jar\nfile2.jar\ndir1/", exitCode = 0) + } + return ShellCommandResult.create("", exitCode = 1) + } + }) + .mockShellCommandResults { command -> + if (command.startsWith("__jetbrains_intellij_get_directory_files")) { + return@mockShellCommandResults ShellCommandResult.create("file1.jar\nfile2.jar\ndir1/", exitCode = 0) + } + return@mockShellCommandResults ShellCommandResult.create("", exitCode = 1) + } + .mockFileSystemSupport(object : ShellFileSystemSupport { + override suspend fun listDirectoryFiles(path: String): List { + return listOf( + ShellFileInfo.create("file1.jar", ShellFileInfo.Type.FILE), + ShellFileInfo.create("file2.jar", ShellFileInfo.Type.FILE), + ShellFileInfo.create("dir1", ShellFileInfo.Type.DIRECTORY), + ) + } + }) + .build() + return fixture + } + + protected suspend fun ShellCompletionTestFixture.getCompletionNames(command: String = "java "): List { + val actual: List = getCompletions(command) + return actual.map { it.name } + } + + + private class MockVMOptionsService : VMOptionsService { + override fun getOrComputeOptionsForJdk(javaHome: String): CompletableFuture { + TestCase.assertEquals("/jre/home", javaHome) + return CompletableFuture.completedFuture( + JdkOptionsData( + listOf( + VMOption("settings", null, null, VMOptionKind.Product, null, VMOptionVariant.X), + VMOption("lint", null, null, VMOptionKind.Standard, null, VMOptionVariant.X), + VMOption("experiment", null, null, VMOptionKind.Experimental, null, VMOptionVariant.X), + VMOption("diagnose", null, null, VMOptionKind.Diagnostic, null, VMOptionVariant.X), + VMOption("advanced", null, null, VMOptionKind.Product, null, VMOptionVariant.XX), + VMOption("add-opens", null, null, VMOptionKind.Product, null, VMOptionVariant.DASH_DASH), + VMOption("add-exports", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), + VMOption("add-experiment-exports", null, null, VMOptionKind.Experimental, null, VMOptionVariant.DASH_DASH), + VMOption("add-diagnostic-exports", null, null, VMOptionKind.Experimental, null, VMOptionVariant.DASH_DASH), + ) + ) + ) + } + + override fun getOrComputeOptionsForJavac(javaHome: String): CompletableFuture { + TestCase.assertEquals("/jre/home", javaHome) + return CompletableFuture.completedFuture( + JdkOptionsData( + listOf( + VMOption("boot-class-path", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), + VMOption("enable-preview", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), + VMOption("release", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), + VMOption("source", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), + VMOption("target", null, null, VMOptionKind.Standard, null, VMOptionVariant.DASH_DASH), + VMOption("experimental", null, null, VMOptionKind.Experimental, null, VMOptionVariant.DASH_DASH), + ) + ) + ) + } + } +}