diff --git a/src/org/elixir_lang/Elixir.kt b/src/org/elixir_lang/Elixir.kt index c01442195..7416688b6 100644 --- a/src/org/elixir_lang/Elixir.kt +++ b/src/org/elixir_lang/Elixir.kt @@ -14,15 +14,16 @@ object Elixir { environment: Map, workingDirectory: String?, elixirSdk: Sdk, - erlArgumentList: kotlin.collections.List = emptyList() + erlArgumentList: kotlin.collections.List = emptyList(), ): GeneralCommandLine { val erlangSdk = elixirSdkToEnsuredErlangSdk(elixirSdk) - val commandLine = org.elixir_lang.Erl.commandLine( - pty = false, - environment = environment, - workingDirectory = workingDirectory, - erlangSdk = erlangSdk - ) + val commandLine = + org.elixir_lang.Erl.commandLine( + pty = false, + environment = environment, + workingDirectory = workingDirectory, + erlangSdk = erlangSdk, + ) // MUST be before `addElixir` because it ends with `-extra` which turns off argument parsing for `erl` commandLine.addParameters(erlArgumentList) addElixir(commandLine, elixirSdk, erlangSdk) @@ -35,13 +36,17 @@ object Elixir { ?: throw MissingErlangSdk(elixirSdk) fun elixirSdkHasErlangSdk(elixirSdk: Sdk): Boolean = elixirSdkToErlangSdk(elixirSdk) != null - fun elixirSdkToErlangSdk(elixirSdk: Sdk): Sdk? = - elixirSdk.sdkAdditionalData?.let { it as SdkAdditionalData }?.erlangSdk + + fun elixirSdkToErlangSdk(elixirSdk: Sdk): Sdk? = elixirSdk.sdkAdditionalData?.let { it as SdkAdditionalData }?.getErlangSdk() /** * Adds `-pa ebinDirectory` for those in the `elixirSdk` that aren't in the `erlangSdk` */ - fun prependNewCodePaths(commandLine: GeneralCommandLine, elixirSdk: Sdk, erlangSdk: Sdk) { + fun prependNewCodePaths( + commandLine: GeneralCommandLine, + elixirSdk: Sdk, + erlangSdk: Sdk, + ) { val elixirEbinDirectories = elixirSdk.ebinDirectories() val erlangEbinDirectories = erlangSdk.ebinDirectories() prependNewCodePaths(commandLine, elixirEbinDirectories, erlangEbinDirectories) @@ -50,7 +55,11 @@ object Elixir { /** * Keep in-suync with [org.elixir_lang.jps.Builder.addElixir] */ - private fun addElixir(commandLine: GeneralCommandLine, elixirSdk: Sdk, erlangSdk: Sdk) { + private fun addElixir( + commandLine: GeneralCommandLine, + elixirSdk: Sdk, + erlangSdk: Sdk, + ) { prependNewCodePaths(commandLine, elixirSdk, erlangSdk) commandLine.addParameters("-noshell", "-s", "elixir", "start_cli") commandLine.addParameters("-elixir", "ansi_enabled", "true") @@ -60,7 +69,7 @@ object Elixir { private fun prependNewCodePaths( commandLine: GeneralCommandLine, elixirEbinDirectories: kotlin.collections.List, - erlangEbinDirectories: kotlin.collections.List + erlangEbinDirectories: kotlin.collections.List, ) { val newEbinDirectories = elixirEbinDirectories - erlangEbinDirectories prependCodePaths(commandLine, newEbinDirectories) diff --git a/src/org/elixir_lang/facet/configurable/Provider.kt b/src/org/elixir_lang/facet/configurable/Provider.kt index a76211739..72ca33ff5 100644 --- a/src/org/elixir_lang/facet/configurable/Provider.kt +++ b/src/org/elixir_lang/facet/configurable/Provider.kt @@ -5,6 +5,6 @@ import com.intellij.openapi.options.ConfigurableProvider import org.elixir_lang.sdk.ProcessOutput class Provider(private val project: com.intellij.openapi.project.Project): ConfigurableProvider() { - override fun canCreateConfigurable(): Boolean = ProcessOutput.isSmallIde() + override fun canCreateConfigurable(): Boolean = ProcessOutput.isSmallIde override fun createConfigurable(): Configurable = Project(project) } diff --git a/src/org/elixir_lang/facet/sdks/Provider.kt b/src/org/elixir_lang/facet/sdks/Provider.kt index 3392861be..47176e260 100644 --- a/src/org/elixir_lang/facet/sdks/Provider.kt +++ b/src/org/elixir_lang/facet/sdks/Provider.kt @@ -4,5 +4,5 @@ import com.intellij.openapi.options.ConfigurableProvider import org.elixir_lang.sdk.ProcessOutput abstract class Provider : ConfigurableProvider() { - override fun canCreateConfigurable(): Boolean = ProcessOutput.isSmallIde() + override fun canCreateConfigurable(): Boolean = ProcessOutput.isSmallIde } diff --git a/src/org/elixir_lang/notification/setup_sdk/Provider.kt b/src/org/elixir_lang/notification/setup_sdk/Provider.kt index a4a3675eb..8dc5329e1 100644 --- a/src/org/elixir_lang/notification/setup_sdk/Provider.kt +++ b/src/org/elixir_lang/notification/setup_sdk/Provider.kt @@ -1,6 +1,7 @@ package org.elixir_lang.notification.setup_sdk import com.intellij.ide.actions.ShowSettingsUtilImpl +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleType @@ -14,6 +15,8 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.elixir_lang.ElixirFileType import org.elixir_lang.ElixirLanguage import org.elixir_lang.sdk.ProcessOutput @@ -21,46 +24,54 @@ import org.elixir_lang.sdk.elixir.Type import java.util.function.Function import javax.swing.JComponent -/** - * https://github.com/ignatov/intellij-erlang/blob/master/src/org/intellij/erlang/inspection/SetupSDKNotificationProvider.java - */ class Provider : EditorNotificationProvider { override fun collectNotificationData( project: Project, - file: VirtualFile + file: VirtualFile, ): Function = - Function { createNotificationPanel(file, project) } + Function { fileEditor -> + kotlinx.coroutines.runBlocking { + createNotificationPanel(file, project) + } + } - private fun createNotificationPanel( + private suspend fun createNotificationPanel( virtualFile: VirtualFile, - project: Project + project: Project, ): EditorNotificationPanel? = - if (virtualFile.fileType is ElixirFileType) { - PsiManager - .getInstance(project) - .findFile(virtualFile) - ?.let { psiFile -> - if (psiFile.language === ElixirLanguage && - Type.mostSpecificSdk(psiFile) == null - ) { - createPanel(project, psiFile) - } else { - null - } + withContext(Dispatchers.Default) { + ReadAction.compute { + if (virtualFile.fileType is ElixirFileType) { + PsiManager + .getInstance(project) + .findFile(virtualFile) + ?.let { psiFile -> + if (psiFile.language === ElixirLanguage && + Type.mostSpecificSdk(psiFile) == null + ) { + createPanel(project, psiFile) + } else { + null + } + } + } else { + null } - } else { - null + } } companion object { fun showFacetSettings(project: Project) { - if (ProcessOutput.isSmallIde()) { + if (ProcessOutput.isSmallIde) { showSmallIDEFacetSettings(project) } // TODO Elixir Facet in non-Elixir module in IntelliJ } - fun showModuleSettings(project: Project, module: Module) { + fun showModuleSettings( + project: Project, + module: Module, + ) { ProjectSettingsService.getInstance(project).openModuleSettings(module) } @@ -78,63 +89,62 @@ class Provider : EditorNotificationProvider { ShowSettingsUtilImpl.showSettingsDialog(project, "language", "Elixir") } - private fun createSmallIDEFacetPanel(project: Project): EditorNotificationPanel { - return EditorNotificationPanel().apply { + private fun createSmallIDEFacetPanel(project: Project): EditorNotificationPanel = + EditorNotificationPanel().apply { text = "Elixir Facet SDK is not defined" @Suppress("DialogTitleCapitalization") createActionLabel("Setup Elixir Facet SDK") { showSmallIDEFacetSettings(project) } } - } - private fun createFacetPanel(project: Project): EditorNotificationPanel? { - return if (ProcessOutput.isSmallIde()) { + private fun createFacetPanel(project: Project): EditorNotificationPanel? = + if (ProcessOutput.isSmallIde) { createSmallIDEFacetPanel(project) } else { // TODO Elixir Facet in non-Elixir module in IntelliJ null } - } - private fun createModulePanel(project: Project, module: Module): EditorNotificationPanel { - return EditorNotificationPanel().apply { + private fun createModulePanel( + project: Project, + module: Module, + ): EditorNotificationPanel = + EditorNotificationPanel().apply { text = "Elixir Module SDK is not defined" @Suppress("DialogTitleCapitalization") createActionLabel("Setup Elixir Module SDK") { showModuleSettings(project, module) } } - } - private fun createPanel(project: Project, psiFile: PsiFile): EditorNotificationPanel? { + private fun createPanel( + project: Project, + psiFile: PsiFile, + ): EditorNotificationPanel? { val module = ModuleUtilCore.findModuleForPsiElement(psiFile) - return if (module != null) { - // CANNOT use ModuleType.is(module, ElixirModuleType.getInstance()) as ElixirModuleType depends on - // JavaModuleBuilder and so only available in IntelliJ - if (ModuleType.get(module).id == "ELIXIR_MODULE") { - createModulePanel(project, module) - } else { - createFacetPanel(project) - } - } else { - if (ProcessOutput.isSmallIde()) { - createSmallIDEFacetPanel(project) - } else { - createProjectPanel(project) + return when { + module != null -> { + // CANNOT use ModuleType.is(module, ElixirModuleType.getInstance()) as ElixirModuleType depends on + // JavaModuleBuilder and so only available in IntelliJ + if (ModuleType.get(module).id == "ELIXIR_MODULE") { + createModulePanel(project, module) + } else { + createFacetPanel(project) + } } + ProcessOutput.isSmallIde -> createSmallIDEFacetPanel(project) + else -> createProjectPanel(project) } } - private fun createProjectPanel(project: Project): EditorNotificationPanel { - return EditorNotificationPanel().apply { + private fun createProjectPanel(project: Project): EditorNotificationPanel = + EditorNotificationPanel().apply { text = "Project SDK is not defined" createActionLabel(ProjectBundle.message("project.sdk.setup")) { showProjectSettings(project) } } - - } } } diff --git a/src/org/elixir_lang/sdk/ProcessOutput.java b/src/org/elixir_lang/sdk/ProcessOutput.java deleted file mode 100644 index 689c07ca4..000000000 --- a/src/org/elixir_lang/sdk/ProcessOutput.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.elixir_lang.sdk; - -import com.google.common.base.Charsets; -import com.intellij.execution.ExecutionException; -import com.intellij.execution.configurations.GeneralCommandLine; -import com.intellij.execution.process.CapturingProcessHandler; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.util.Function; -import com.intellij.util.PlatformUtils; -import com.intellij.util.containers.ContainerUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.List; - -/** - * Created by zyuyou on 2015/5/27. - * - */ -public class ProcessOutput { - /* - * CONSTANTS - */ - - private static final Logger LOGGER = Logger.getInstance(ProcessOutput.class); - public static final int STANDARD_TIMEOUT = 10 * 1000; - - @Nullable - public static T transformStdoutLine(@NotNull com.intellij.execution.process.ProcessOutput output, @NotNull Function lineTransformer) { - List lines; - - if (output.getExitCode() != 0 || output.isTimeout() || output.isCancelled()) { - lines = ContainerUtil.emptyList(); - } else { - lines = output.getStdoutLines(); - } - - T transformed = null; - - for (String line : lines) { - transformed = lineTransformer.fun(line); - - if (transformed != null) { - break; - } - } - - return transformed; - } - - @Nullable - public static T transformStdoutLine(@NotNull Function lineTransformer, - int timeout, - @NotNull String workDir, - @NotNull String exePath, - @NotNull String... arguments) { - T transformed = null; - - try { - com.intellij.execution.process.ProcessOutput output = getProcessOutput(timeout, workDir, exePath, arguments); - - transformed = transformStdoutLine(output, lineTransformer); - } catch (ExecutionException executionException) { - LOGGER.warn(executionException); - } - - return transformed; - } - - @NotNull - public static com.intellij.execution.process.ProcessOutput getProcessOutput(int timeout, - @Nullable String workDir, - @NotNull String exePath, - @NotNull String... arguments) throws ExecutionException{ - if(workDir == null || !new File(workDir).isDirectory() || !new File(exePath).canExecute()){ - return new com.intellij.execution.process.ProcessOutput(); - } - - GeneralCommandLine cmd = new GeneralCommandLine().withCharset(Charsets.UTF_8); - cmd.withWorkDirectory(workDir); - cmd.setExePath(exePath); - cmd.addParameters(arguments); - - return execute(cmd, timeout); - } - - @NotNull - public static com.intellij.execution.process.ProcessOutput execute(@NotNull GeneralCommandLine cmd) throws ExecutionException { - return execute(cmd, STANDARD_TIMEOUT); - } - - @NotNull - public static com.intellij.execution.process.ProcessOutput execute(@NotNull GeneralCommandLine cmd, int timeout) throws ExecutionException { - CapturingProcessHandler processHandler = new CapturingProcessHandler(cmd); - return timeout < 0 ? processHandler.runProcess() : processHandler.runProcess(timeout); - } - - public static boolean isSmallIde(){ - return !(PlatformUtils.isIntelliJ() || PlatformUtils.getPlatformPrefix().equals("AndroidStudio")); - } -} diff --git a/src/org/elixir_lang/sdk/ProcessOutput.kt b/src/org/elixir_lang/sdk/ProcessOutput.kt new file mode 100644 index 000000000..6f969182a --- /dev/null +++ b/src/org/elixir_lang/sdk/ProcessOutput.kt @@ -0,0 +1,102 @@ +package org.elixir_lang.sdk + +import com.google.common.base.Charsets +import com.intellij.execution.ExecutionException +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.openapi.diagnostic.Logger +import com.intellij.util.Function +import com.intellij.util.PlatformUtils +import com.intellij.util.containers.ContainerUtil +import java.io.File + +/** + * Created by zyuyou on 2015/5/27. + * + */ +object ProcessOutput { + /* + * CONSTANTS + */ + private val LOGGER = Logger.getInstance( + ProcessOutput::class.java + ) + const val STANDARD_TIMEOUT: Int = 10 * 1000 + + fun transformStdoutLine( + output: com.intellij.execution.process.ProcessOutput, + lineTransformer: Function + ): T? { + val lines = if (output.exitCode != 0 || output.isTimeout || output.isCancelled) { + ContainerUtil.emptyList() + } else { + output.stdoutLines + } + + var transformed: T? = null + + for (line in lines) { + transformed = lineTransformer.`fun`(line) + + if (transformed != null) { + break + } + } + + return transformed + } + + fun transformStdoutLine( + lineTransformer: Function, + timeout: Int, + workDir: String, + exePath: String, + vararg arguments: String? + ): T? { + var transformed: T? = null + + try { + val output = getProcessOutput(timeout, workDir, exePath, *arguments) + + transformed = transformStdoutLine(output, lineTransformer) + } catch (executionException: ExecutionException) { + LOGGER.warn(executionException) + } + + return transformed + } + + @JvmStatic + @Throws(ExecutionException::class) + fun getProcessOutput( + timeout: Int, + workDir: String?, + exePath: String, + vararg arguments: String? + ): com.intellij.execution.process.ProcessOutput { + if (workDir == null || !File(workDir).isDirectory || !File(exePath).canExecute()) { + return com.intellij.execution.process.ProcessOutput() + } + + val cmd = GeneralCommandLine().withCharset(Charsets.UTF_8) + cmd.withWorkDirectory(workDir) + cmd.exePath = exePath + cmd.addParameters(*arguments) + + return execute(cmd, timeout) + } + + @JvmOverloads + @Throws(ExecutionException::class) + fun execute( + cmd: GeneralCommandLine, + timeout: Int = STANDARD_TIMEOUT + ): com.intellij.execution.process.ProcessOutput { + val processHandler = CapturingProcessHandler(cmd) + return if (timeout < 0) processHandler.runProcess() else processHandler.runProcess(timeout) + } + + @JvmStatic + val isSmallIde: Boolean + get() = !(PlatformUtils.isIntelliJ() || PlatformUtils.getPlatformPrefix() == "AndroidStudio") +} diff --git a/src/org/elixir_lang/sdk/Type.java b/src/org/elixir_lang/sdk/Type.java deleted file mode 100644 index 22b3f1a03..000000000 --- a/src/org/elixir_lang/sdk/Type.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.elixir_lang.sdk; - -import com.intellij.openapi.projectRoots.SdkModificator; -import com.intellij.openapi.roots.JavadocOrderRootType; -import com.intellij.openapi.roots.OrderRootType; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.function.Consumer; - -import static org.elixir_lang.jps.HomePath.eachEbinPath; -import static org.elixir_lang.sdk.ProcessOutput.isSmallIde; - -public class Type { - private Type() { - } - - public static void addCodePaths(@NotNull SdkModificator sdkModificator) { - eachEbinPath( - sdkModificator.getHomePath(), - ebin -> ebinPathChainVirtualFile( - ebin, virtualFile -> sdkModificator.addRoot(virtualFile, OrderRootType.CLASSES) - ) - ); - } - - public static void ebinPathChainVirtualFile(@NotNull Path ebinPath, Consumer virtualFileConsumer) { - VirtualFile virtualFile = LocalFileSystem - .getInstance() - .findFileByIoFile(ebinPath.toFile()); - - if (virtualFile != null) { - virtualFileConsumer.accept(virtualFile); - } - } - - @Nullable - public static OrderRootType documentationRootType() { - OrderRootType rootType; - - if (isSmallIde()) { - rootType = null; - } else { - rootType = JavadocOrderRootType.getInstance(); - } - - return rootType; - } -} diff --git a/src/org/elixir_lang/sdk/Type.kt b/src/org/elixir_lang/sdk/Type.kt new file mode 100644 index 000000000..e2dd03f53 --- /dev/null +++ b/src/org/elixir_lang/sdk/Type.kt @@ -0,0 +1,46 @@ +package org.elixir_lang.sdk + +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.roots.JavadocOrderRootType +import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import org.elixir_lang.jps.HomePath +import java.nio.file.Path +import java.util.function.Consumer + +object Type { + fun addCodePaths(sdkModificator: SdkModificator) { + HomePath.eachEbinPath( + sdkModificator.homePath + ) { ebin: Path -> + ebinPathChainVirtualFile( + ebin + ) { virtualFile: VirtualFile? -> + sdkModificator.addRoot( + virtualFile!!, OrderRootType.CLASSES + ) + } + } + } + + fun ebinPathChainVirtualFile(ebinPath: Path, virtualFileConsumer: Consumer) { + val virtualFile = LocalFileSystem + .getInstance() + .findFileByIoFile(ebinPath.toFile()) + + if (virtualFile != null) { + virtualFileConsumer.accept(virtualFile) + } + } + + fun documentationRootType(): OrderRootType? { + val rootType = if (ProcessOutput.isSmallIde) { + null + } else { + JavadocOrderRootType.getInstance() + } + + return rootType + } +} diff --git a/src/org/elixir_lang/sdk/elixir/Release.java b/src/org/elixir_lang/sdk/elixir/Release.java deleted file mode 100644 index a6c75c88c..000000000 --- a/src/org/elixir_lang/sdk/elixir/Release.java +++ /dev/null @@ -1,185 +0,0 @@ -package org.elixir_lang.sdk.elixir; - -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class Release implements Comparable { - /* - * CONSTANTS - */ - - public static final Release V_1_0_4 = new Release("1", "0", "4", null, null); - public static final Release LATEST = new Release("1", "6", "0", "dev", null); - - private static final Pattern VERSION_PATTERN = Pattern.compile( - // @version_regex from Version in elixir itself - "(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\-([\\d\\w\\.\\-]+))?(?:\\+([\\d\\w\\-]+))?" - ); - - /* - * - * Static Methods - * - */ - - /* - * Public Static Methods - */ - - @Nullable - public static Release fromString(@Nullable String versionString){ - Matcher m = versionString != null ? VERSION_PATTERN.matcher(versionString) : null; - return m != null && m.matches() ? new Release(m.group(1), m.group(2), m.group(3), m.group(4), m.group(5)) : null; - } - - /* - * Private Static Methods - */ - - @Contract(pure = true) - private static int compareMaybeFormattedDecimals(@Nullable String mine, @Nullable String others) { - int comparison; - - if (mine == null && others == null) { - comparison = 0; - } else if (mine == null) { - comparison = -1; - } else if (others == null) { - comparison = 1; - } else { - try { - int myInt = Integer.parseInt(mine); - - try { - int othersInt = Integer.parseInt(others); - - if (myInt > othersInt) { - comparison = 1; - } else if (myInt < othersInt) { - comparison = -1; - } else { - comparison = 0; - } - } catch (NumberFormatException numberFormatException) { - comparison = mine.compareTo(others); - } - } catch (NumberFormatException numberFormatException) { - comparison = mine.compareTo(others); - } - } - - return comparison; - } - - /** - * @see Version.to_compare - */ - @Contract(pure = true) - private static int comparePre(@Nullable String mine, @Nullable String others) { - int comparison; - - if (mine == null && others == null) { - comparison = 0; - } else if (mine == null) { - // https://github.com/elixir-lang/elixir/blob/27c350da06ee4df5a4710507abe443ffba5b07dd/lib/elixir/lib/version.ex#L203 - comparison = 1; - } else if (others == null) { - // https://github.com/elixir-lang/elixir/blob/27c350da06ee4df5a4710507abe443ffba5b07dd/lib/elixir/lib/version.ex#L204 - comparison = -1; - } else { - comparison = mine.compareTo(others); - } - - return comparison; - } - - /* - * Fields - */ - - @Nullable - private final String build; - @NotNull - public final String major; - @Nullable - public final String minor; - @Nullable - private final String patch; - @Nullable - private final String pre; - - /* - * Constructors - */ - - public Release(@NotNull String major, - @Nullable String minor, - @Nullable String patch, - @Nullable String pre, - @Nullable String build) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.pre = pre; - this.build = build; - - if (minor == null && patch != null) { - throw new IllegalArgumentException("patch MUST be null if minor is null"); - } - } - - /* - * Instance Methods - */ - - @Override - public int compareTo(@NotNull Release other) { - int comparison = compareMaybeFormattedDecimals(major, other.major); - - if (comparison == 0) { - comparison = compareMaybeFormattedDecimals(minor, other.minor); - - if (comparison == 0) { - comparison = compareMaybeFormattedDecimals(patch, other.patch); - - if (comparison == 0) { - comparison = comparePre(pre, other.pre); - } - } - } - - return comparison; - } - - @Override - public String toString() { - return "Elixir " + version(); - } - - @NotNull - public String version(){ - StringBuilder version = new StringBuilder(major); - - if (minor != null) { - version.append('.').append(minor); - } - - if (patch != null) { - version.append('.').append(patch); - } - - if (pre != null) { - version.append('-').append(pre); - } - - if (build != null) { - version.append('+').append(build); - } - - return version.toString(); - } -} diff --git a/src/org/elixir_lang/sdk/elixir/Release.kt b/src/org/elixir_lang/sdk/elixir/Release.kt new file mode 100644 index 000000000..eccceda0a --- /dev/null +++ b/src/org/elixir_lang/sdk/elixir/Release.kt @@ -0,0 +1,86 @@ +package org.elixir_lang.sdk.elixir + +import org.jetbrains.annotations.Contract +import java.util.regex.Pattern + +class Release private constructor( + @JvmField val major: String, + @JvmField val minor: String?, + private val patch: String?, + private val pre: String?, + private val build: String? +) : Comparable { + + companion object { + @JvmField val V_1_0_4 = Release("1", "0", "4", null, null) + @JvmField val LATEST = Release("1", "6", "0", "dev", null) + + private val VERSION_PATTERN = Pattern.compile( + "(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\-([\\d\\w\\.\\-]+))?(?:\\+([\\d\\w\\-]+))?" + ) + + @JvmStatic + fun fromString(versionString: String?): Release? { + val m = versionString?.let { VERSION_PATTERN.matcher(it) } + return if (m != null && m.matches()) { + Release(m.group(1), m.group(2), m.group(3), m.group(4), m.group(5)) + } else null + } + + @Contract(pure = true) + private fun compareMaybeFormattedDecimals(mine: String?, others: String?): Int { + return when { + mine == null && others == null -> 0 + mine == null -> -1 + others == null -> 1 + else -> try { + val myInt = mine.toInt() + val othersInt = others.toInt() + myInt.compareTo(othersInt) + } catch (numberFormatException: NumberFormatException) { + mine.compareTo(others) + } + } + } + + @Contract(pure = true) + private fun comparePre(mine: String?, others: String?): Int { + return when { + mine == null && others == null -> 0 + mine == null -> 1 + others == null -> -1 + else -> mine.compareTo(others) + } + } + } + + init { + require(!(minor == null && patch != null)) { "patch MUST be null if minor is null" } + } + + override fun compareTo(other: Release): Int { + var comparison = compareMaybeFormattedDecimals(major, other.major) + if (comparison == 0) { + comparison = compareMaybeFormattedDecimals(minor, other.minor) + if (comparison == 0) { + comparison = compareMaybeFormattedDecimals(patch, other.patch) + if (comparison == 0) { + comparison = comparePre(pre, other.pre) + } + } + } + return comparison + } + + override fun toString(): String = "Elixir ${version()}" + + fun version(): String { + return buildString { + append(major) + minor?.let { append('.').append(it) } + patch?.let { append('.').append(it) } + pre?.let { append('-').append(it) } + build?.let { append('+').append(it) } + } + } +} \ No newline at end of file diff --git a/src/org/elixir_lang/sdk/elixir/Type.kt b/src/org/elixir_lang/sdk/elixir/Type.kt index 8f1404245..1d37a6c5b 100644 --- a/src/org/elixir_lang/sdk/elixir/Type.kt +++ b/src/org/elixir_lang/sdk/elixir/Type.kt @@ -28,26 +28,25 @@ import com.intellij.serviceContainer.AlreadyDisposedException import com.intellij.util.system.CpuArch import gnu.trove.THashSet import org.apache.commons.io.FilenameUtils -import org.apache.commons.lang.ArrayUtils import org.elixir_lang.Facet import org.elixir_lang.Icons import org.elixir_lang.jps.HomePath import org.elixir_lang.jps.model.SerializerExtension import org.elixir_lang.jps.sdk_type.Elixir import org.elixir_lang.sdk.ProcessOutput +import org.elixir_lang.sdk.Type.ebinPathChainVirtualFile import org.elixir_lang.sdk.erlang_dependent.AdditionalDataConfigurable import org.elixir_lang.sdk.erlang_dependent.SdkAdditionalData -import org.elixir_lang.sdk.erlang_dependent.SdkModificatorRootTypeConsumer import org.jdom.Element import org.jetbrains.annotations.Contract -import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.TestOnly import java.io.File import java.nio.file.Path import java.nio.file.Paths import java.util.* import javax.swing.Icon -import java.util.concurrent.Callable +import kotlin.Comparator +import kotlin.collections.ArrayList class Type : org.elixir_lang.sdk.erlang_dependent.Type(SerializerExtension.ELIXIR_SDK_TYPE_ID) { /** @@ -102,8 +101,7 @@ class Type : org.elixir_lang.sdk.erlang_dependent.Type(SerializerExtension.ELIXI override fun getPresentableName(): String = "Elixir SDK" - override fun getVersionString(sdkHome: String): String = - Release.fromString(File(sdkHome).name)?.version() ?: "Unknown" + override fun getVersionString(sdkHome: String): String = Release.fromString(File(sdkHome).name)?.version() ?: "Unknown" /** * Map of home paths to versions in descending version order so that newer versions are favored. @@ -114,32 +112,47 @@ class Type : org.elixir_lang.sdk.erlang_dependent.Type(SerializerExtension.ELIXI val homePathByVersion: MutableMap = TreeMap(Comparator.reverseOrder()) if (SystemInfo.isMac) { HomePath.mergeASDF(homePathByVersion, "elixir") - HomePath.mergeHomebrew(homePathByVersion, "elixir", java.util.function.Function.identity()) - HomePath.mergeNixStore(homePathByVersion, NIX_PATTERN, java.util.function.Function.identity()) + HomePath.mergeHomebrew( + homePathByVersion, + "elixir", + java.util.function.Function + .identity(), + ) + HomePath.mergeNixStore( + homePathByVersion, + NIX_PATTERN, + java.util.function.Function + .identity(), + ) } else { val sdkPath: String if (SystemInfo.isWindows) { - sdkPath = if (CpuArch.CURRENT.width == 32) { - WINDOWS_32BIT_DEFAULT_HOME_PATH - } else { - WINDOWS_64BIT_DEFAULT_HOME_PATH - } + sdkPath = + if (CpuArch.CURRENT.width == 32) { + WINDOWS_32BIT_DEFAULT_HOME_PATH + } else { + WINDOWS_64BIT_DEFAULT_HOME_PATH + } homePathByVersion[HomePath.UNKNOWN_VERSION] = sdkPath } else if (SystemInfo.isLinux) { putIfDirectory(homePathByVersion, HomePath.UNKNOWN_VERSION, LINUX_DEFAULT_HOME_PATH) putIfDirectory(homePathByVersion, HomePath.UNKNOWN_VERSION, LINUX_MINT_HOME_PATH) - HomePath.mergeNixStore(homePathByVersion, NIX_PATTERN, java.util.function.Function.identity()) + HomePath.mergeNixStore( + homePathByVersion, + NIX_PATTERN, + java.util.function.Function + .identity(), + ) } } return homePathByVersion } - private fun invalidSdkHomeException(virtualFile: VirtualFile): Exception { - return Exception(invalidSdkHomeMessage(virtualFile)) - } + private fun invalidSdkHomeException(virtualFile: VirtualFile): Exception = Exception(invalidSdkHomeMessage(virtualFile)) - private fun invalidSdkHomeMessage(virtualFile: VirtualFile): String = if (virtualFile.isDirectory) { - """A valid home for $presentableName has the following structure: + private fun invalidSdkHomeMessage(virtualFile: VirtualFile): String = + if (virtualFile.isDirectory) { + """A valid home for $presentableName has the following structure: ELIXIR_SDK_HOME * bin @@ -155,17 +168,20 @@ ELIXIR_SDK_HOME ** logger ** mix """ - } else { - "A directory must be select for the home for $presentableName" - } + } else { + "A directory must be select for the home for $presentableName" + } override fun isValidSdkHome(path: String): Boolean { val elixir = Elixir.getScriptInterpreterExecutable(path) val elixirc = Elixir.getByteCodeCompilerExecutable(path) val iex = Elixir.getIExExecutable(path) val mix = Elixir.mixFile(path) - return elixir.canExecute() && elixirc.canExecute() && iex.canExecute() && mix.canRead() && - HomePath.hasEbinPath(path) + return elixir.canExecute() && + elixirc.canExecute() && + iex.canExecute() && + mix.canRead() && + HomePath.hasEbinPath(path) } override fun setupSdkPaths(sdk: Sdk) { @@ -181,12 +197,12 @@ ELIXIR_SDK_HOME return suggestedHomePath } - override fun suggestHomePaths(): Collection { - return homePathByVersion().values - } + override fun suggestHomePaths(): Collection = homePathByVersion().values - override fun suggestSdkName(currentSdkName: String?, sdkHome: String): String = - Release.fromString(File(sdkHome).name)?.toString() ?: "Elixir at $sdkHome" + override fun suggestSdkName( + currentSdkName: String?, + sdkHome: String, + ): String = Release.fromString(File(sdkHome).name)?.toString() ?: "Elixir at $sdkHome" private fun validateSdkHomePath(virtualFile: VirtualFile) { val selectedPath = virtualFile.path @@ -201,14 +217,12 @@ ELIXIR_SDK_HOME override fun createAdditionalDataConfigurable( sdkModel: SdkModel, - sdkModificator: SdkModificator - ): com.intellij.openapi.projectRoots.AdditionalDataConfigurable { - return AdditionalDataConfigurable(sdkModel, sdkModificator) - } + sdkModificator: SdkModificator, + ): com.intellij.openapi.projectRoots.AdditionalDataConfigurable = AdditionalDataConfigurable(sdkModel, sdkModificator) override fun saveAdditionalData( additionalData: com.intellij.openapi.projectRoots.SdkAdditionalData, - additional: Element + additional: Element, ) { if (additionalData is SdkAdditionalData) { try { @@ -222,7 +236,7 @@ ELIXIR_SDK_HOME @Suppress("SameParameterValue") override fun loadAdditionalData( elixirSdk: Sdk, - additional: Element + additional: Element, ): com.intellij.openapi.projectRoots.SdkAdditionalData { val sdkAdditionalData = SdkAdditionalData(elixirSdk) try { @@ -248,7 +262,7 @@ ELIXIR_SDK_HOME private fun addDocumentationPath( sdkModificator: SdkModificator, releaseVersion: String?, - appName: String + appName: String, ) { val hexdocUrlBuilder = StringBuilder("https://hexdoc.pm/").append(appName) if (releaseVersion != null) { @@ -256,7 +270,9 @@ ELIXIR_SDK_HOME } val hexdocUrlVirtualFile = VirtualFileManager.getInstance().findFileByUrl(hexdocUrlBuilder.toString()) if (hexdocUrlVirtualFile != null) { - val documentationRootType = org.elixir_lang.sdk.Type.documentationRootType() + val documentationRootType = + org.elixir_lang.sdk.Type + .documentationRootType() if (documentationRootType != null) { sdkModificator.addRoot(hexdocUrlVirtualFile, documentationRootType) } @@ -266,7 +282,7 @@ ELIXIR_SDK_HOME private fun addDocumentationPath( sdkModificator: SdkModificator, releaseVersion: String?, - ebinPath: Path + ebinPath: Path, ) { val appName = ebinPath.parent.fileName.toString() addDocumentationPath(sdkModificator, releaseVersion, appName) @@ -275,24 +291,30 @@ ELIXIR_SDK_HOME private fun addDocumentationPaths(sdkModificator: SdkModificator) { val releaseVersion = releaseVersion(sdkModificator) HomePath.eachEbinPath( - sdkModificator.homePath + sdkModificator.homePath, ) { ebinPath: Path -> addDocumentationPath(sdkModificator, releaseVersion, ebinPath) } } private fun addSourcePaths(sdkModificator: SdkModificator) { HomePath.eachEbinPath( - sdkModificator.homePath + sdkModificator.homePath, ) { ebinPath: Path -> addSourcePath(sdkModificator, ebinPath) } } - private fun addSourcePath(sdkModificator: SdkModificator, libFile: File) { + private fun addSourcePath( + sdkModificator: SdkModificator, + libFile: File, + ) { val sourcePath = VfsUtil.findFileByIoFile(libFile, true) if (sourcePath != null) { sdkModificator.addRoot(sourcePath, OrderRootType.SOURCES) } } - private fun addSourcePath(sdkModificator: SdkModificator, ebinPath: Path) { + private fun addSourcePath( + sdkModificator: SdkModificator, + ebinPath: Path, + ) { val parentPath = ebinPath.parent val libPath = Paths.get(parentPath.toString(), "lib") val libFile = libPath.toFile() @@ -303,14 +325,18 @@ ELIXIR_SDK_HOME private fun configureSdkPaths(sdk: Sdk) { val sdkModificator = sdk.sdkModificator - org.elixir_lang.sdk.Type.addCodePaths(sdkModificator) + org.elixir_lang.sdk.Type + .addCodePaths(sdkModificator) addDocumentationPaths(sdkModificator) addSourcePaths(sdkModificator) configureInternalErlangSdk(sdk, sdkModificator) ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() } } - private fun configureInternalErlangSdk(elixirSdk: Sdk, elixirSdkModificator: SdkModificator): Sdk? { + private fun configureInternalErlangSdk( + elixirSdk: Sdk, + elixirSdkModificator: SdkModificator, + ): Sdk? { val erlangSdk = defaultErlangSdk() if (erlangSdk != null) { val sdkAdditionData: com.intellij.openapi.projectRoots.SdkAdditionalData = @@ -325,17 +351,17 @@ ELIXIR_SDK_HOME fun addNewCodePathsFromInternErlangSdk( elixirSdk: Sdk, internalErlangSdk: Sdk, - elixirSdkModificator: SdkModificator + elixirSdkModificator: SdkModificator, ) { codePathsFromInternalErlangSdk( elixirSdk, internalErlangSdk, elixirSdkModificator, - SdkModificatorRootTypeConsumer { sdkModificator: SdkModificator, configuredRoots: Array, expandedInternalRoot: VirtualFile, type: OrderRootType -> - if (!ArrayUtils.contains(configuredRoots, expandedInternalRoot)) { + { sdkModificator, configuredRoots, expandedInternalRoot, type -> + if (expandedInternalRoot !in configuredRoots) { sdkModificator.addRoot(expandedInternalRoot, type) } - } + }, ) } @@ -343,18 +369,15 @@ ELIXIR_SDK_HOME fun removeCodePathsFromInternalErlangSdk( elixirSdk: Sdk, internalErlangSdk: Sdk, - elixirSdkModificator: SdkModificator + elixirSdkModificator: SdkModificator, ) { codePathsFromInternalErlangSdk( elixirSdk, internalErlangSdk, elixirSdkModificator, - SdkModificatorRootTypeConsumer { sdkModificator: SdkModificator, _: Array, expandedInternalRoot: VirtualFile, type: OrderRootType -> - sdkModificator.removeRoot( - expandedInternalRoot, - type - ) - } + { sdkModificator, _, expandedInternalRoot, type -> + sdkModificator.removeRoot(expandedInternalRoot, type) + }, ) } @@ -362,7 +385,7 @@ ELIXIR_SDK_HOME elixirSdk: Sdk, internalErlangSdk: Sdk, elixirSdkModificator: SdkModificator, - sdkModificatorRootTypeConsumer: SdkModificatorRootTypeConsumer + sdkModificatorRootTypeConsumer: (SdkModificator, Array, VirtualFile, OrderRootType) -> Unit, ) { val internalSdkType = internalErlangSdk.sdkType as SdkType val elixirSdkType = elixirSdk.sdkType as SdkType @@ -372,8 +395,7 @@ ELIXIR_SDK_HOME val configuredRoots = elixirSdkModificator.getRoots(type) for (internalRoot in internalRoots) { for (expandedInternalRoot in expandInternalErlangSdkRoot(internalRoot, type)) { - sdkModificatorRootTypeConsumer - .consume(elixirSdkModificator, configuredRoots, expandedInternalRoot, type) + sdkModificatorRootTypeConsumer(elixirSdkModificator, configuredRoots, expandedInternalRoot, type) } } } @@ -382,7 +404,7 @@ ELIXIR_SDK_HOME private fun expandInternalErlangSdkRoot( internalRoot: VirtualFile, - type: OrderRootType + type: OrderRootType, ): Iterable { val expandedInternalRootList: List if (type === OrderRootType.CLASSES) { @@ -398,9 +420,11 @@ ELIXIR_SDK_HOME expandedInternalRootList = ArrayList() val parentPath = Paths.get(path).parent.toString() HomePath.eachEbinPath(parentPath) { ebinPath: Path? -> - org.elixir_lang.sdk.Type.ebinPathChainVirtualFile( - ebinPath!! - ) { e: VirtualFile -> expandedInternalRootList.add(e) } + ebinPathChainVirtualFile( + ebinPath!!, + ) { virtualFile: VirtualFile? -> + virtualFile?.let { expandedInternalRootList.add(it) } + } } } else { expandedInternalRootList = listOf(internalRoot) @@ -413,7 +437,10 @@ ELIXIR_SDK_HOME @JvmStatic @TestOnly - fun createMockSdk(sdkHome: String, release: Release): Sdk { + fun createMockSdk( + sdkHome: String, + release: Release, + ): Sdk { val sdk: Sdk = ProjectJdkImpl(release.toString(), instance) val sdkModificator = sdk.sdkModificator sdkModificator.homePath = sdkHome @@ -434,7 +461,7 @@ ELIXIR_SDK_HOME private fun createDefaultErlangSdk( projectJdkTable: ProjectJdkTable, erlangSdkType: SdkType, - homePath: String + homePath: String, ): Sdk? { val sdkName = erlangSdkType.suggestSdkName("Default " + erlangSdkType.name, homePath) val projectJdkImpl = ProjectJdkImpl(sdkName, erlangSdkType) @@ -446,7 +473,7 @@ ELIXIR_SDK_HOME { ApplicationManager.getApplication().runWriteAction { projectJdkTable.addJdk(projectJdkImpl) } }, - ModalityState.NON_MODAL + ModalityState.NON_MODAL, ) projectJdkImpl } else { @@ -456,20 +483,21 @@ ELIXIR_SDK_HOME private fun createDefaultErlangSdk( projectJdkTable: ProjectJdkTable, - erlangSdkType: SdkType + erlangSdkType: SdkType, ): Sdk? = defaultErlangSdkHomePath()?.let { homePath -> createDefaultErlangSdk(projectJdkTable, erlangSdkType, homePath) } @JvmStatic - fun erlangSdkType(): SdkType = if (ProcessOutput.isSmallIde()) { + fun erlangSdkType(): SdkType = + if (ProcessOutput.isSmallIde) { /* intellij-erlang's "Erlang SDK" does not work in small IDEs because it uses JavadocRoot for documentation, which isn't available in Small IDEs. */ - null - } else { - EP_NAME.extensionList.find { sdkType -> sdkType.name == "Erlang SDK" } - } ?: findInstance(org.elixir_lang.sdk.erlang.Type::class.java) + null + } else { + EP_NAME.extensionList.find { sdkType -> sdkType.name == "Erlang SDK" } + } ?: findInstance(org.elixir_lang.sdk.erlang.Type::class.java) private fun defaultErlangSdk(): Sdk? { val projectJdkTable = ProjectJdkTable.getInstance() @@ -489,19 +517,20 @@ ELIXIR_SDK_HOME @JvmStatic @Contract("null -> null") - fun getRelease(sdk: Sdk?): Release? = if (sdk != null && sdk.sdkType === instance) { - Release.fromString(sdk.versionString) - ?: sdk.homePath?.let { Release.fromString(File(it).name) } - } else { - null - } + fun getRelease(sdk: Sdk?): Release? = + if (sdk != null && sdk.sdkType === instance) { + Release.fromString(sdk.versionString) + ?: sdk.homePath?.let { Release.fromString(File(it).name) } + } else { + null + } private fun getVersionString(version: Release?): String? = version?.toString() private fun putIfDirectory( homePathByVersion: MutableMap, @Suppress("SameParameterValue") version: Version, - homePath: String + homePath: String, ) { val homeFile = File(homePath) if (homeFile.isDirectory) { @@ -513,11 +542,12 @@ ELIXIR_SDK_HOME private fun projectSdk(project: Project): Sdk? = sdk(ProjectRootManager.getInstance(project).projectSdk) - private fun sdk(sdk: Sdk?): Sdk? = if (sdk != null && sdk.sdkType === instance) { - sdk - } else { - null - } + private fun sdk(sdk: Sdk?): Sdk? = + if (sdk != null && sdk.sdkType === instance) { + sdk + } else { + null + } @JvmStatic fun mostSpecificSdk(module: Module): Sdk? = @@ -529,17 +559,20 @@ ELIXIR_SDK_HOME val project = psiElement.project return if (!project.isDisposed) { - /* ModuleUtilCore.findModuleForPsiElement can fail with NullPointerException if the - ProjectFileIndex.SERVICE.getInstance(Project) returns {@code null}, so check that the - ProjectFileIndex is available first */ if (ProjectFileIndex.SERVICE.getInstance(project) != null) { - val module = try { - ReadAction.compute { - ModuleUtilCore.findModuleForPsiElement(psiElement) - } - } catch (_: AlreadyDisposedException) { - null - } + // Use a background thread to perform the ReadAction + val module = + ApplicationManager + .getApplication() + .executeOnPooledThread { + try { + ReadAction.compute { + ModuleUtilCore.findModuleForPsiElement(psiElement) + } + } catch (_: AlreadyDisposedException) { + null + } + }.get() // Wait for the result if (module != null) { mostSpecificSdk(module) diff --git a/src/org/elixir_lang/sdk/erlang/Release.java b/src/org/elixir_lang/sdk/erlang/Release.java deleted file mode 100644 index af291e7f0..000000000 --- a/src/org/elixir_lang/sdk/erlang/Release.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.elixir_lang.sdk.erlang; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Release { - private static final Pattern VERSION_PATTERN = Pattern.compile("Erlang/OTP (\\S+) \\[erts-(\\S+)\\]"); - - @NotNull - final String otpRelease; - @NotNull - private final String ertsVersion; - - Release(@NotNull String otpRelease, @NotNull String ertsVersion) { - this.otpRelease = otpRelease; - this.ertsVersion = ertsVersion; - } - - @Nullable - static Release fromString(@Nullable String versionString) { - Release release = null; - - if (versionString != null) { - Matcher matcher = VERSION_PATTERN.matcher(versionString); - - if (matcher.matches()) { - release = new Release(matcher.group(1), matcher.group(2)); - } - } - - return release; - } - - @NotNull - @Override - public String toString() { - return "Erlang/OTP " + otpRelease + " [erts-" + ertsVersion + "]"; - } -} diff --git a/src/org/elixir_lang/sdk/erlang/Release.kt b/src/org/elixir_lang/sdk/erlang/Release.kt new file mode 100644 index 000000000..b3161a5d0 --- /dev/null +++ b/src/org/elixir_lang/sdk/erlang/Release.kt @@ -0,0 +1,27 @@ +package org.elixir_lang.sdk.erlang + +import java.util.regex.Pattern + +class Release internal constructor(val otpRelease: String, private val ertsVersion: String) { + override fun toString(): String { + return "Erlang/OTP $otpRelease [erts-$ertsVersion]" + } + + companion object { + private val VERSION_PATTERN: Pattern = Pattern.compile("Erlang/OTP (\\S+) \\[erts-(\\S+)\\]") + + fun fromString(versionString: String?): Release? { + var release: Release? = null + + if (versionString != null) { + val matcher = VERSION_PATTERN.matcher(versionString) + + if (matcher.matches()) { + release = Release(matcher.group(1), matcher.group(2)) + } + } + + return release + } + } +} diff --git a/src/org/elixir_lang/sdk/erlang/Type.java b/src/org/elixir_lang/sdk/erlang/Type.java deleted file mode 100644 index b98e212e2..000000000 --- a/src/org/elixir_lang/sdk/erlang/Type.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.elixir_lang.sdk.erlang; - -import com.intellij.execution.ExecutionException; -import com.intellij.execution.process.ProcessOutput; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.SdkModel; -import com.intellij.openapi.projectRoots.SdkModificator; -import com.intellij.openapi.projectRoots.SdkType; -import com.intellij.openapi.roots.OrderRootType; -import com.intellij.openapi.util.SystemInfo; -import com.intellij.openapi.util.Version; -import com.intellij.util.containers.ContainerUtil; -import org.elixir_lang.jps.sdk_type.Erlang; -import org.elixir_lang.jps.HomePath; -import org.elixir_lang.sdk.erlang_dependent.AdditionalDataConfigurable; -import org.jdom.Element; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.*; -import java.util.function.Function; -import java.util.regex.Pattern; - -import static org.elixir_lang.jps.HomePath.*; -import static org.elixir_lang.sdk.Type.addCodePaths; -import static org.elixir_lang.sdk.Type.documentationRootType; - -/** - * An Erlang SdkType for use when `intellij-erlang` is not installed - */ -public class Type extends SdkType { - private static final String OTP_RELEASE_PREFIX_LINE = Type.class.getCanonicalName() + " OTP_RELEASE:"; - private static final String ERTS_VERSION_PREFIX_LINE = Type.class.getCanonicalName() + " ERTS_VERSION:"; - private static final String PRINT_VERSION_INFO_EXPRESSION = - "io:format(\"~n~s~n~s~n~s~n~s~n\",[" + - "\"" + OTP_RELEASE_PREFIX_LINE + "\"," + - "erlang:system_info(otp_release)," + - "\"" + ERTS_VERSION_PREFIX_LINE + "\"," + - "erlang:system_info(version)" + - "]),erlang:halt()."; - private static final String WINDOWS_DEFAULT_HOME_PATH = "C:\\Program Files\\erl9.0"; - private static final Pattern NIX_PATTERN = nixPattern("erlang"); - private static final String LINUX_MINT_HOME_PATH = HomePath.LINUX_MINT_HOME_PATH + "/erlang"; - private static final String LINUX_DEFAULT_HOME_PATH = HomePath.LINUX_DEFAULT_HOME_PATH + "/erlang"; - private static final Function VERSION_PATH_TO_HOME_PATH = - versionPath -> new File(versionPath, "lib/erlang"); - private static final Logger LOGGER = Logger.getInstance(Type.class); - private final Map releaseBySdkHome = ContainerUtil.createWeakMap(); - - - public Type() { - // Don't use "Erlang SDK" as we want it to be different from the name in intellij-erlang - super("Erlang SDK for Elixir SDK"); - } - - @NotNull - private static String getDefaultSdkName(@NotNull String sdkHome, @Nullable Release version) { - StringBuilder defaultSdkNameBuilder = new StringBuilder("Erlang for Elixir "); - - if (version != null) { - defaultSdkNameBuilder.append(version.otpRelease); - } else { - defaultSdkNameBuilder.append(" at ").append(sdkHome); - } - - return defaultSdkNameBuilder.toString(); - } - - @Nullable - private static String getVersionCacheKey(@Nullable String sdkHome) { - String versionCacheKey = null; - - if (sdkHome != null) { - versionCacheKey = new File(sdkHome).getAbsolutePath(); - } - - return versionCacheKey; - } - - @Nullable - private static Release parseSdkVersion(@NotNull List printVersionInfoOutput) { - String otpRelease = null; - String ertsVersion = null; - - ListIterator iterator = printVersionInfoOutput.listIterator(); - - while (iterator.hasNext()) { - String line = iterator.next(); - - if (OTP_RELEASE_PREFIX_LINE.equals(line) && iterator.hasNext()) { - otpRelease = iterator.next(); - } else if (ERTS_VERSION_PREFIX_LINE.equals(line) && iterator.hasNext()) { - ertsVersion = iterator.next(); - } - } - - Release release; - - if (otpRelease != null && ertsVersion != null) { - release = new Release(otpRelease, ertsVersion); - } else { - release = null; - } - - return release; - } - - /** - * Map of home paths to versions in descending version order so that newer versions are favored. - * - * @return Map - */ - public static Map homePathByVersion() { - Map homePathByVersion = HomePath.homePathByVersion(); - - if (SystemInfo.isMac) { - mergeASDF(homePathByVersion, "erlang"); - mergeHomebrew(homePathByVersion, "erlang", VERSION_PATH_TO_HOME_PATH); - mergeNixStore(homePathByVersion, NIX_PATTERN, VERSION_PATH_TO_HOME_PATH); - } else { - if (SystemInfo.isWindows) { - putIfDirectory(homePathByVersion, UNKNOWN_VERSION, WINDOWS_DEFAULT_HOME_PATH); - } else if (SystemInfo.isLinux) { - putIfDirectory(homePathByVersion, UNKNOWN_VERSION, LINUX_DEFAULT_HOME_PATH); - putIfDirectory(homePathByVersion, UNKNOWN_VERSION, LINUX_MINT_HOME_PATH); - - mergeTravisCIKerl(homePathByVersion, Function.identity()); - mergeNixStore(homePathByVersion, NIX_PATTERN, VERSION_PATH_TO_HOME_PATH); - } - } - - return homePathByVersion; - } - - private static void putIfDirectory(@NotNull Map homePathByVersion, - @NotNull Version version, - @NotNull String homePath) { - File homeFile = new File(homePath); - - if (homeFile.isDirectory()) { - homePathByVersion.put(version, homePath); - } - } - - @Override - public boolean isRootTypeApplicable(@NotNull OrderRootType type) { - return type == OrderRootType.CLASSES || - type == OrderRootType.SOURCES || - type == documentationRootType(); - } - - @Override - public void setupSdkPaths(@NotNull Sdk sdk) { - SdkModificator sdkModificator = sdk.getSdkModificator(); - addCodePaths(sdkModificator); - ApplicationManager.getApplication().runWriteAction(sdkModificator::commitChanges); - } - - /** - * Returns a recommended starting path for a file chooser (where SDKs of this type are usually may be found), - * or {@code null} if not applicable/no SDKs found. - *

- * E.g. for Python SDK on Unix the method may return either {@code "/usr/bin"} or {@code "/usr/bin/python"} - * (if there is only one Python interpreter installed on a host). - */ - @Nullable - @Override - public String suggestHomePath() { - Iterator iterator = suggestHomePaths().iterator(); - String suggestedHomePath = null; - - if (iterator.hasNext()) { - suggestedHomePath = iterator.next(); - } - - return suggestedHomePath; - } - - @NotNull - @Override - public Collection suggestHomePaths() { - return homePathByVersion().values(); - } - - @Override - public boolean isValidSdkHome(String path) { - File erl = Erlang.getByteCodeInterpreterExecutable(path); - - return erl.canExecute(); - } - - @Override - public String suggestSdkName(String currentSdkName, String sdkHome) { - return getDefaultSdkName(sdkHome, detectSdkVersion(sdkHome)); - } - - @Nullable - @Override - public String getVersionString(@NotNull String sdkHome) { - Release release = detectSdkVersion(sdkHome); - String versionString; - - if (release != null) { - versionString = release.otpRelease; - } else { - versionString = null; - } - - return versionString; - } - - @Nullable - @Override - public AdditionalDataConfigurable createAdditionalDataConfigurable(@NotNull SdkModel sdkModel, - @NotNull SdkModificator sdkModificator) { - return null; - } - - @NotNull - @Override - public String getPresentableName() { - return getName(); - } - - @Override - public void saveAdditionalData(@NotNull com.intellij.openapi.projectRoots.SdkAdditionalData additionalData, - @NotNull Element additional) { - // Intentionally left blank - } - - @Nullable - private Release detectSdkVersion(@NotNull String sdkHome) { - Release release = null; - - Release cachedRelease = releaseBySdkHome.get(getVersionCacheKey(sdkHome)); - - if (cachedRelease != null) { - release = cachedRelease; - } else { - File erl = Erlang.getByteCodeInterpreterExecutable(sdkHome); - - if (!erl.canExecute()) { - StringBuilder messageBuilder = new StringBuilder("Can't detect Erlang version: ").append(erl.getPath()); - - if (erl.exists()) { - messageBuilder.append(" is not executable."); - } else { - messageBuilder.append(" is missing."); - } - - LOGGER.warn(messageBuilder.toString()); - } else { - try { - ProcessOutput output = org.elixir_lang.sdk.ProcessOutput.getProcessOutput( - 10 * 1000, - sdkHome, - erl.getAbsolutePath(), - "-noshell", - "-eval", - PRINT_VERSION_INFO_EXPRESSION - ); - - if (!(output.getExitCode() != 0 || output.isCancelled() || output.isTimeout())) { - release = parseSdkVersion(output.getStdoutLines()); - } - - if (release != null) { - releaseBySdkHome.put(getVersionCacheKey(sdkHome), release); - } else { - LOGGER.warn("Failed to detect Erlang version.\n" + - "StdOut: " + output.getStdout() + "\n" + - "StdErr: " + output.getStderr()); - } - } catch (ExecutionException e) { - LOGGER.warn(e); - } - } - } - - return release; - } -} diff --git a/src/org/elixir_lang/sdk/erlang/Type.kt b/src/org/elixir_lang/sdk/erlang/Type.kt new file mode 100644 index 000000000..99a263292 --- /dev/null +++ b/src/org/elixir_lang/sdk/erlang/Type.kt @@ -0,0 +1,196 @@ +package org.elixir_lang.sdk.erlang + +import com.intellij.execution.ExecutionException +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkModel +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.projectRoots.SdkType +import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.Version +import com.intellij.util.containers.ContainerUtil +import org.elixir_lang.jps.HomePath +import org.elixir_lang.jps.sdk_type.Erlang +import org.elixir_lang.sdk.erlang_dependent.AdditionalDataConfigurable +import org.jdom.Element +import java.io.File + +class Type : SdkType("Erlang SDK for Elixir SDK") { + private val releaseBySdkHome: MutableMap = ContainerUtil.createWeakMap() + + companion object { + private const val OTP_RELEASE_PREFIX_LINE = "org.elixir_lang.sdk.erlang.Type OTP_RELEASE:" + private const val ERTS_VERSION_PREFIX_LINE = "org.elixir_lang.sdk.erlang.Type ERTS_VERSION:" + private const val PRINT_VERSION_INFO_EXPRESSION = + "io:format(\"~n~s~n~s~n~s~n~s~n\",[" + + "\"$OTP_RELEASE_PREFIX_LINE\"," + + "erlang:system_info(otp_release)," + + "\"$ERTS_VERSION_PREFIX_LINE\"," + + "erlang:system_info(version)" + + "]),erlang:halt()." + private const val WINDOWS_DEFAULT_HOME_PATH = "C:\\Program Files\\erl9.0" + private val NIX_PATTERN = HomePath.nixPattern("erlang") + private const val LINUX_MINT_HOME_PATH = "${HomePath.LINUX_MINT_HOME_PATH}/erlang" + private const val LINUX_DEFAULT_HOME_PATH = "${HomePath.LINUX_DEFAULT_HOME_PATH}/erlang" + private val VERSION_PATH_TO_HOME_PATH: (File) -> File = { versionPath -> File(versionPath, "lib/erlang") } + private val LOGGER = Logger.getInstance(Type::class.java) + + @JvmStatic + fun getDefaultSdkName( + sdkHome: String, + version: Release?, + ): String = + buildString { + append("Erlang for Elixir ") + if (version != null) { + append(version.otpRelease) + } else { + append(" at ").append(sdkHome) + } + } + + private fun getVersionCacheKey(sdkHome: String?): String? = sdkHome?.let { File(it).absolutePath } + + private fun parseSdkVersion(printVersionInfoOutput: List): Release? { + var otpRelease: String? = null + var ertsVersion: String? = null + + val iterator = printVersionInfoOutput.listIterator() + while (iterator.hasNext()) { + when (val line = iterator.next()) { + OTP_RELEASE_PREFIX_LINE -> if (iterator.hasNext()) otpRelease = iterator.next() + ERTS_VERSION_PREFIX_LINE -> if (iterator.hasNext()) ertsVersion = iterator.next() + } + } + + return if (otpRelease != null && ertsVersion != null) Release(otpRelease, ertsVersion) else null + } + + @JvmStatic + fun homePathByVersion(): Map { + val homePathByVersion = HomePath.homePathByVersion().toMutableMap() + + when { + SystemInfo.isMac -> { + HomePath.mergeASDF(homePathByVersion, "erlang") + HomePath.mergeHomebrew(homePathByVersion, "erlang", VERSION_PATH_TO_HOME_PATH) + HomePath.mergeNixStore(homePathByVersion, NIX_PATTERN, VERSION_PATH_TO_HOME_PATH) + } + SystemInfo.isWindows -> { + putIfDirectory(homePathByVersion, HomePath.UNKNOWN_VERSION, WINDOWS_DEFAULT_HOME_PATH) + } + SystemInfo.isLinux -> { + putIfDirectory(homePathByVersion, HomePath.UNKNOWN_VERSION, LINUX_DEFAULT_HOME_PATH) + putIfDirectory(homePathByVersion, HomePath.UNKNOWN_VERSION, LINUX_MINT_HOME_PATH) + HomePath.mergeTravisCIKerl(homePathByVersion) { it } + HomePath.mergeNixStore(homePathByVersion, NIX_PATTERN, VERSION_PATH_TO_HOME_PATH) + } + } + + return homePathByVersion + } + + private fun putIfDirectory( + homePathByVersion: MutableMap, + version: Version, + homePath: String, + ) { + val homeFile = File(homePath) + if (homeFile.isDirectory) { + homePathByVersion[version] = homePath + } + } + } + + override fun isRootTypeApplicable(type: OrderRootType): Boolean = + type == OrderRootType.CLASSES || + type == OrderRootType.SOURCES || + type == + org.elixir_lang.sdk.Type + .documentationRootType() + + override fun setupSdkPaths(sdk: Sdk) { + val sdkModificator = sdk.sdkModificator + org.elixir_lang.sdk.Type + .addCodePaths(sdkModificator) + ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() } + } + + override fun suggestHomePath(): String? = suggestHomePaths().firstOrNull() + + override fun suggestHomePaths(): Collection = homePathByVersion().values + + override fun isValidSdkHome(path: String): Boolean = Erlang.getByteCodeInterpreterExecutable(path).canExecute() + + override fun suggestSdkName( + currentSdkName: String?, + sdkHome: String, + ): String = getDefaultSdkName(sdkHome, detectSdkVersion(sdkHome)) + + override fun getVersionString(sdkHome: String): String? = detectSdkVersion(sdkHome)?.otpRelease + + override fun createAdditionalDataConfigurable( + sdkModel: SdkModel, + sdkModificator: SdkModificator, + ): AdditionalDataConfigurable? = null + + override fun getPresentableName(): String = name + + override fun saveAdditionalData( + additionalData: com.intellij.openapi.projectRoots.SdkAdditionalData, + additional: Element, + ) { + // Intentionally left blank + } + + private fun detectSdkVersion(sdkHome: String): Release? { + val cachedRelease = getVersionCacheKey(sdkHome)?.let { releaseBySdkHome[it] } + if (cachedRelease != null) return cachedRelease + + val erl = Erlang.getByteCodeInterpreterExecutable(sdkHome) + if (!erl.canExecute()) { + val message = + buildString { + append("Can't detect Erlang version: ${erl.path}") + if (erl.exists()) append(" is not executable.") else append(" is missing.") + } + LOGGER.warn(message) + return null + } + + var release: Release? = null + + ApplicationManager + .getApplication() + .executeOnPooledThread { + try { + val output = + org.elixir_lang.sdk.ProcessOutput.getProcessOutput( + 10 * 1000, + sdkHome, + erl.absolutePath, + "-noshell", + "-eval", + PRINT_VERSION_INFO_EXPRESSION, + ) + + if (output.exitCode == 0 && !output.isCancelled && !output.isTimeout) { + release = + parseSdkVersion(output.stdoutLines)?.also { detectedRelease -> + getVersionCacheKey(sdkHome)?.let { key -> + releaseBySdkHome[key] = detectedRelease + } + } + } else { + LOGGER.warn("Failed to detect Erlang version.\nStdOut: ${output.stdout}\nStdErr: ${output.stderr}") + } + } catch (e: ExecutionException) { + LOGGER.warn(e) + } + }.get() // Wait for the task to complete + + return release + } +} diff --git a/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.java b/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.java deleted file mode 100644 index 46bc2bbff..000000000 --- a/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.java +++ /dev/null @@ -1,243 +0,0 @@ -package org.elixir_lang.sdk.erlang_dependent; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.options.ConfigurationException; -import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.SdkModel; -import com.intellij.openapi.projectRoots.SdkModificator; -import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl; -import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.util.Comparing; -import com.intellij.ui.SimpleListCellRenderer; -import com.intellij.util.ui.JBUI; -import org.elixir_lang.sdk.elixir.Type; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ItemEvent; -import java.util.Objects; - -import static org.elixir_lang.sdk.elixir.Type.addNewCodePathsFromInternErlangSdk; -import static org.elixir_lang.sdk.elixir.Type.removeCodePathsFromInternalErlangSdk; -import static org.elixir_lang.sdk.erlang_dependent.Type.staticIsValidDependency; - -public class AdditionalDataConfigurable implements com.intellij.openapi.projectRoots.AdditionalDataConfigurable { - private final JLabel internalErlangSdkLabel = new JLabel("Internal Erlang SDK:"); - private final DefaultComboBoxModel internalErlangSdksComboBoxModel = new DefaultComboBoxModel<>(); - private final ComboBox internalErlangSdksComboBox = new ComboBox(internalErlangSdksComboBoxModel); - private final SdkModel sdkModel; - private final SdkModel.Listener sdkModelListener; - private final SdkModificator sdkModificator; - private Sdk elixirSdk; - private boolean modified; - private boolean freeze = false; - - public AdditionalDataConfigurable(@NotNull SdkModel sdkModel, SdkModificator sdkModificator) { - this.sdkModel = sdkModel; - this.sdkModificator = sdkModificator; - - sdkModelListener = new SdkModel.Listener() { - public void sdkAdded(Sdk sdk) { - if (staticIsValidDependency(sdk)) { - addErlangSdk(sdk); - } - } - - public void beforeSdkRemove(Sdk sdk) { - if (staticIsValidDependency(sdk)) { - removeErlangSdk(sdk); - } - } - - public void sdkChanged(Sdk sdk, String previousName) { - if (staticIsValidDependency(sdk)) { - updateErlangSdkList(sdk, previousName); - } - } - - public void sdkHomeSelected(final Sdk sdk, final String newSdkHome) { - if (staticIsValidDependency(sdk)) { - internalErlangSdkUpdate(sdk); - } - } - }; - this.sdkModel.addListener(sdkModelListener); - } - - private void updateJdkList() { - internalErlangSdksComboBoxModel.removeAllElements(); - - for (Sdk sdk : sdkModel.getSdks()) { - if (staticIsValidDependency(sdk)) { - internalErlangSdksComboBoxModel.addElement(sdk); - } - } - } - - public void setSdk(Sdk sdk) { - elixirSdk = sdk; - } - - public JComponent createComponent() { - JPanel wholePanel = new JPanel(new GridBagLayout()); - - wholePanel.add( - internalErlangSdkLabel, - new GridBagConstraints( - 0, - GridBagConstraints.RELATIVE, - 1, - 1, - 0, - 1, - GridBagConstraints.WEST, - GridBagConstraints.NONE, - JBUI.emptyInsets(), - 0, - 0 - ) - ); - wholePanel.add( - internalErlangSdksComboBox, - new GridBagConstraints( - 1, - GridBagConstraints.RELATIVE, - 1, - 1, - 1, - 1, - GridBagConstraints.EAST, - GridBagConstraints.HORIZONTAL, - JBUI.insets(0, 30, 0, 0), - 0, - 0 - ) - ); - internalErlangSdksComboBox.setRenderer( - new SimpleListCellRenderer() { - @Override - public void customize(JList list, Object value, int index, boolean selected, boolean hasFocus) { - if (value instanceof Sdk) { - setText(((Sdk) value).getName()); - } - } - } - ); - internalErlangSdksComboBox.addItemListener(itemEvent -> { - if (!freeze) { - final int stateChange = itemEvent.getStateChange(); - final Sdk internalErlangSdk = (Sdk) itemEvent.getItem(); - - if (stateChange == ItemEvent.DESELECTED) { - removeCodePathsFromInternalErlangSdk(elixirSdk, internalErlangSdk, sdkModificator); - } else if (stateChange == ItemEvent.SELECTED) { - addNewCodePathsFromInternErlangSdk(elixirSdk, internalErlangSdk, sdkModificator); - modified = true; - } - } - }); - - modified = true; - - return wholePanel; - } - - private void internalErlangSdkUpdate(@NotNull final Sdk sdk) { - SdkAdditionalData sdkAdditionalData = (SdkAdditionalData) sdk.getSdkAdditionalData(); - Sdk erlangSdk; - - if (sdkAdditionalData != null) { - erlangSdk = sdkAdditionalData.getErlangSdk(); - } else { - erlangSdk = null; - } - - if (erlangSdk == null || internalErlangSdksComboBoxModel.getIndexOf(erlangSdk) == -1) { - internalErlangSdksComboBoxModel.addElement(erlangSdk); - } else { - internalErlangSdksComboBoxModel.setSelectedItem(erlangSdk); - } - } - - public boolean isModified() { - return modified; - } - - public void apply() throws ConfigurationException { - writeInternalErlangSdk((Sdk) internalErlangSdksComboBox.getSelectedItem()); - modified = false; - } - - private void writeInternalErlangSdk(@Nullable Sdk erlangSdk) { - writeInternalErlangSdk(elixirSdk.getSdkModificator(), erlangSdk); - } - - private void writeInternalErlangSdk(@NotNull SdkModificator sdkModificator, @Nullable Sdk erlangSdk) { - SdkAdditionalData sdkAdditionData = new SdkAdditionalData( - erlangSdk, - elixirSdk - ); - sdkModificator.setSdkAdditionalData(sdkAdditionData); - ApplicationManager.getApplication().runWriteAction(sdkModificator::commitChanges); - ((ProjectJdkImpl) elixirSdk).resetVersionString(); - } - - public void reset() { - freeze = true; - updateJdkList(); - freeze = false; - - if (elixirSdk != null && elixirSdk.getSdkAdditionalData() instanceof SdkAdditionalData) { - final SdkAdditionalData sdkAdditionalData = (SdkAdditionalData) elixirSdk.getSdkAdditionalData(); - final Sdk erlangSdk = sdkAdditionalData.getErlangSdk(); - - if (erlangSdk != null) { - for (int i = 0; i < internalErlangSdksComboBoxModel.getSize(); i++) { - if (Comparing.strEqual( - internalErlangSdksComboBoxModel.getElementAt(i).getName(), - erlangSdk.getName() - )) { - internalErlangSdksComboBox.setSelectedIndex(i); - break; - } - } - } - - modified = false; - } - } - - public void disposeUIResources() { - sdkModel.removeListener(sdkModelListener); - } - - private void addErlangSdk(final Sdk sdk) { - internalErlangSdksComboBoxModel.addElement(sdk); - } - - private void removeErlangSdk(final Sdk sdk) { - if (internalErlangSdksComboBoxModel.getSelectedItem().equals(sdk)) { - modified = true; - } - - internalErlangSdksComboBoxModel.removeElement(sdk); - } - - private void updateErlangSdkList(Sdk sdk, String previousName) { - final Sdk[] sdks = sdkModel.getSdks(); - - for (Sdk currentSdk : sdks) { - if (currentSdk.getSdkType() instanceof Type) { - final SdkAdditionalData sdkAdditionalData = (SdkAdditionalData) currentSdk.getSdkAdditionalData(); - final Sdk erlangSdk = sdkAdditionalData.getErlangSdk(); - - if (erlangSdk != null && Objects.equals(erlangSdk.getName(), previousName)) { - sdkAdditionalData.setErlangSdk(sdk); - } - } - } - updateJdkList(); - } -} diff --git a/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt b/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt new file mode 100644 index 000000000..f8675fc4c --- /dev/null +++ b/src/org/elixir_lang/sdk/erlang_dependent/AdditionalDataConfigurable.kt @@ -0,0 +1,256 @@ +package org.elixir_lang.sdk.erlang_dependent + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.projectRoots.AdditionalDataConfigurable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkModel +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.Comparing +import com.intellij.ui.SimpleListCellRenderer +import com.intellij.util.ui.JBUI +import org.elixir_lang.sdk.elixir.Type.Companion.addNewCodePathsFromInternErlangSdk +import org.elixir_lang.sdk.elixir.Type.Companion.removeCodePathsFromInternalErlangSdk +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.event.ItemEvent +import javax.swing.* + +class AdditionalDataConfigurable( + private val sdkModel: SdkModel, + private val sdkModificator: SdkModificator, +) : AdditionalDataConfigurable { + private val internalErlangSdkLabel = JLabel("Internal Erlang SDK:") + private val internalErlangSdksComboBoxModel = DefaultComboBoxModel() + private val internalErlangSdksComboBox = ComboBox(internalErlangSdksComboBoxModel) + private val sdkModelListener: SdkModel.Listener + private var elixirSdk: Sdk? = null + private var modified = false + private var freeze = false + + init { + sdkModelListener = + object : SdkModel.Listener { + override fun sdkAdded(sdk: Sdk) { + if (Type.staticIsValidDependency(sdk)) { + addErlangSdk(sdk) + } + } + + override fun beforeSdkRemove(sdk: Sdk) { + if (Type.staticIsValidDependency(sdk)) { + removeErlangSdk(sdk) + } + } + + override fun sdkChanged( + sdk: Sdk, + previousName: String, + ) { + if (Type.staticIsValidDependency(sdk)) { + updateErlangSdkList(sdk, previousName) + } + } + + override fun sdkHomeSelected( + sdk: Sdk, + newSdkHome: String, + ) { + if (Type.staticIsValidDependency(sdk)) { + internalErlangSdkUpdate(sdk) + } + } + } + sdkModel.addListener(sdkModelListener) + } + + private fun updateJdkList() { + internalErlangSdksComboBoxModel.removeAllElements() + + for (sdk in sdkModel.sdks) { + if (Type.staticIsValidDependency(sdk)) { + internalErlangSdksComboBoxModel.addElement(sdk) + } + } + } + + override fun setSdk(sdk: Sdk) { + elixirSdk = sdk + } + + override fun createComponent(): JComponent { + val wholePanel = JPanel(GridBagLayout()) + + wholePanel.add( + internalErlangSdkLabel, + GridBagConstraints( + 0, + GridBagConstraints.RELATIVE, + 1, + 1, + 0.0, + 1.0, + GridBagConstraints.WEST, + GridBagConstraints.NONE, + JBUI.emptyInsets(), + 0, + 0, + ), + ) + wholePanel.add( + internalErlangSdksComboBox, + GridBagConstraints( + 1, + GridBagConstraints.RELATIVE, + 1, + 1, + 1.0, + 1.0, + GridBagConstraints.EAST, + GridBagConstraints.HORIZONTAL, + JBUI.insetsLeft(30), + 0, + 0, + ), + ) + internalErlangSdksComboBox.setRenderer( + object : SimpleListCellRenderer() { + override fun customize( + list: JList, + value: Sdk?, + index: Int, + selected: Boolean, + hasFocus: Boolean, + ) { + text = value?.name + } + }, + ) + internalErlangSdksComboBox.addItemListener { itemEvent: ItemEvent -> + if (!freeze) { + val stateChange = itemEvent.stateChange + val internalErlangSdk = itemEvent.item as Sdk + + if (stateChange == ItemEvent.DESELECTED) { + removeCodePathsFromInternalErlangSdk( + elixirSdk!!, + internalErlangSdk, + sdkModificator, + ) + } else if (stateChange == ItemEvent.SELECTED) { + addNewCodePathsFromInternErlangSdk( + elixirSdk!!, + internalErlangSdk, + sdkModificator, + ) + modified = true + } + } + } + + modified = true + + return wholePanel + } + + private fun internalErlangSdkUpdate(sdk: Sdk) { + val sdkAdditionalData = sdk.sdkAdditionalData as SdkAdditionalData? + + val erlangSdk = sdkAdditionalData?.getErlangSdk() + + if (erlangSdk == null || internalErlangSdksComboBoxModel.getIndexOf(erlangSdk) == -1) { + internalErlangSdksComboBoxModel.addElement(erlangSdk) + } else { + internalErlangSdksComboBoxModel.setSelectedItem(erlangSdk) + } + } + + override fun isModified(): Boolean = modified + + @Throws(ConfigurationException::class) + override fun apply() { + writeInternalErlangSdk(internalErlangSdksComboBox.selectedItem as Sdk) + modified = false + } + + private fun writeInternalErlangSdk(erlangSdk: Sdk?) { + writeInternalErlangSdk(elixirSdk!!.sdkModificator, erlangSdk) + } + + private fun writeInternalErlangSdk( + sdkModificator: SdkModificator, + erlangSdk: Sdk?, + ) { + val sdkAdditionData = + SdkAdditionalData( + erlangSdk, + elixirSdk!!, + ) + sdkModificator.sdkAdditionalData = sdkAdditionData + ApplicationManager.getApplication().runWriteAction { sdkModificator.commitChanges() } + (elixirSdk as ProjectJdkImpl).resetVersionString() + } + + override fun reset() { + freeze = true + updateJdkList() + freeze = false + + if (elixirSdk != null && elixirSdk!!.sdkAdditionalData is SdkAdditionalData) { + val sdkAdditionalData = elixirSdk!!.sdkAdditionalData as SdkAdditionalData? + val erlangSdk = sdkAdditionalData!!.getErlangSdk() + + if (erlangSdk != null) { + for (i in 0 until internalErlangSdksComboBoxModel.size) { + if (Comparing.strEqual( + internalErlangSdksComboBoxModel.getElementAt(i)!!.name, + erlangSdk.name, + ) + ) { + internalErlangSdksComboBox.selectedIndex = i + break + } + } + } + + modified = false + } + } + + override fun disposeUIResources() { + sdkModel.removeListener(sdkModelListener) + } + + private fun addErlangSdk(sdk: Sdk) { + internalErlangSdksComboBoxModel.addElement(sdk) + } + + private fun removeErlangSdk(sdk: Sdk) { + if (internalErlangSdksComboBoxModel.selectedItem == sdk) { + modified = true + } + + internalErlangSdksComboBoxModel.removeElement(sdk) + } + + private fun updateErlangSdkList( + sdk: Sdk, + previousName: String, + ) { + val sdks = sdkModel.sdks + + for (currentSdk in sdks) { + if (currentSdk.sdkType is org.elixir_lang.sdk.elixir.Type) { + val sdkAdditionalData = currentSdk.sdkAdditionalData as SdkAdditionalData? + val erlangSdk = sdkAdditionalData!!.getErlangSdk() + + if (erlangSdk != null && erlangSdk.name == previousName) { + sdkAdditionalData.setErlangSdk(sdk) + } + } + } + updateJdkList() + } +} diff --git a/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.java b/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.java deleted file mode 100644 index e06ea72b6..000000000 --- a/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.elixir_lang.sdk.erlang_dependent; - -import com.intellij.openapi.options.ConfigurationException; -import com.intellij.openapi.projectRoots.ProjectJdkTable; -import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.SdkModel; -import com.intellij.openapi.projectRoots.ValidatableSdkAdditionalData; -import com.intellij.openapi.util.InvalidDataException; -import com.intellij.openapi.util.WriteExternalException; -import org.elixir_lang.sdk.elixir.Type; -import org.jdom.Element; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class SdkAdditionalData implements ValidatableSdkAdditionalData { - @NotNull - private final Sdk elixirSdk; - @Nullable - private Sdk erlangSdk; - @Nullable - private String erlangSdkName; - private static final String ERLANG_SDK_NAME = "erlang-sdk-name"; - - public SdkAdditionalData(@Nullable Sdk erlangSdk, @NotNull Sdk elixirSdk) { - this.erlangSdk = erlangSdk; - this.elixirSdk = elixirSdk; - } - - // readExternal - public SdkAdditionalData(@NotNull Sdk elixirSdk) { - this.elixirSdk = elixirSdk; - } - - - /** - * Checks if the ERLANG_SDK_NAME properties are configured correctly, and throws an exception - * if they are not. - * - * @param sdkModel the model containing all configured SDKs. - * @throws ConfigurationException if the ERLANG_SDK_NAME is not configured correctly. - * @since 5.0.1 - */ - @Override - public void checkValid(SdkModel sdkModel) throws ConfigurationException { - if (getErlangSdk() == null) { - throw new ConfigurationException("Please configure the Erlang ERLANG_SDK_NAME"); - } - } - - @Override - public Object clone() throws CloneNotSupportedException { - return new SdkAdditionalData(erlangSdk, elixirSdk); - } - - public void readExternal(Element element) throws InvalidDataException { - erlangSdkName = element.getAttributeValue(ERLANG_SDK_NAME); - } - - public void writeExternal(Element element) throws WriteExternalException { - final Sdk sdk = getErlangSdk(); - - if (sdk != null) { - element.setAttribute(ERLANG_SDK_NAME, sdk.getName()); - } - } - - @Nullable - public Sdk getErlangSdk() { - final ProjectJdkTable jdkTable = ProjectJdkTable.getInstance(); - - if (erlangSdk == null) { - if (erlangSdkName != null) { - erlangSdk = jdkTable.findJdk(erlangSdkName); - erlangSdkName = null; - } else { - for (Sdk jdk : jdkTable.getAllJdks()) { - if (Type.staticIsValidDependency(jdk)) { - erlangSdk = jdk; - break; - } - } - } - } - - return erlangSdk; - } - - public void setErlangSdk(@Nullable Sdk erlangSdk) { - this.erlangSdk = erlangSdk; - } -} diff --git a/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt b/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt new file mode 100644 index 000000000..90b4354ff --- /dev/null +++ b/src/org/elixir_lang/sdk/erlang_dependent/SdkAdditionalData.kt @@ -0,0 +1,88 @@ +package org.elixir_lang.sdk.erlang_dependent + +import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkModel +import com.intellij.openapi.projectRoots.ValidatableSdkAdditionalData +import com.intellij.openapi.util.InvalidDataException +import com.intellij.openapi.util.WriteExternalException +import org.jdom.Element + +class SdkAdditionalData : + ValidatableSdkAdditionalData, + Cloneable { + private val elixirSdk: Sdk + private var erlangSdk: Sdk? = null + private var erlangSdkName: String? = null + + constructor(erlangSdk: Sdk?, elixirSdk: Sdk) { + this.erlangSdk = erlangSdk + this.elixirSdk = elixirSdk + } + + // readExternal + constructor(elixirSdk: Sdk) { + this.elixirSdk = elixirSdk + } + + /** + * Checks if the ERLANG_SDK_NAME properties are configured correctly, and throws an exception + * if they are not. + * + * @param sdkModel the model containing all configured SDKs. + * @throws ConfigurationException if the ERLANG_SDK_NAME is not configured correctly. + * @since 5.0.1 + */ + @Throws(ConfigurationException::class) + override fun checkValid(sdkModel: SdkModel) { + if (getErlangSdk() == null) { + throw ConfigurationException("Please configure the Erlang ERLANG_SDK_NAME") + } + } + + @Throws(CloneNotSupportedException::class) + public override fun clone(): Any = SdkAdditionalData(erlangSdk, elixirSdk) + + @Throws(InvalidDataException::class) + fun readExternal(element: Element) { + erlangSdkName = element.getAttributeValue(ERLANG_SDK_NAME) + } + + @Throws(WriteExternalException::class) + fun writeExternal(element: Element) { + val sdk = getErlangSdk() + + if (sdk != null) { + element.setAttribute(ERLANG_SDK_NAME, sdk.name) + } + } + + fun getErlangSdk(): Sdk? { + val jdkTable = ProjectJdkTable.getInstance() + + if (erlangSdk == null) { + if (erlangSdkName != null) { + erlangSdk = jdkTable.findJdk(erlangSdkName!!) + erlangSdkName = null + } else { + for (jdk in jdkTable.allJdks) { + if (Type.staticIsValidDependency(jdk)) { + erlangSdk = jdk + break + } + } + } + } + + return erlangSdk + } + + fun setErlangSdk(erlangSdk: Sdk?) { + this.erlangSdk = erlangSdk + } + + companion object { + private const val ERLANG_SDK_NAME = "erlang-sdk-name" + } +} diff --git a/src/org/elixir_lang/sdk/erlang_dependent/SdkModificatorRootTypeConsumer.java b/src/org/elixir_lang/sdk/erlang_dependent/SdkModificatorRootTypeConsumer.java deleted file mode 100644 index d1e9199f5..000000000 --- a/src/org/elixir_lang/sdk/erlang_dependent/SdkModificatorRootTypeConsumer.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.elixir_lang.sdk.erlang_dependent; - -import com.intellij.openapi.projectRoots.SdkModificator; -import com.intellij.openapi.roots.OrderRootType; -import com.intellij.openapi.vfs.VirtualFile; -import org.jetbrains.annotations.NotNull; - -public interface SdkModificatorRootTypeConsumer { - void consume(@NotNull SdkModificator sdkModificator, - @NotNull VirtualFile[] configuredRoots, - @NotNull VirtualFile expandedInternalRoot, - @NotNull OrderRootType type); -} diff --git a/src/org/elixir_lang/sdk/erlang_dependent/SdkModificatorRootTypeConsumer.kt b/src/org/elixir_lang/sdk/erlang_dependent/SdkModificatorRootTypeConsumer.kt new file mode 100644 index 000000000..3c3364e33 --- /dev/null +++ b/src/org/elixir_lang/sdk/erlang_dependent/SdkModificatorRootTypeConsumer.kt @@ -0,0 +1,14 @@ +package org.elixir_lang.sdk.erlang_dependent + +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.vfs.VirtualFile + +interface SdkModificatorRootTypeConsumer { + fun consume( + sdkModificator: SdkModificator, + configuredRoots: Array?, + expandedInternalRoot: VirtualFile, + type: OrderRootType + ) +}