From b0e8f9adbfd224b99ac5754b52b9bc6bc77d950c Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Sat, 15 Jul 2017 15:19:27 -0500 Subject: [PATCH] Use separate formatter for Elixir >= 1.4.0 Use a seperate formatter, one GenEvent-based and one GenServer-based, depending on whether the SDK is >= 1.4.0 as that's when GenEvent was deprecated and GenServer was preferred. --- .../1.1.0/team_city_ex_unit_formatter.ex | 22 + .../1.4.0/team_city_ex_unit_formatter.ex | 17 + ...ter.ex => team_city_ex_unit_formatting.ex} | 149 +++-- .../debugger/xdebug/ElixirXDebugProcess.java | 11 +- src/org/elixir_lang/exunit/ElixirModules.java | 78 ++- .../mix/runner/MixRunningState.java | 6 +- .../runner/exunit/MixExUnitRunningState.java | 15 +- src/org/elixir_lang/sdk/ElixirSdkType.java | 608 +++++++++--------- 8 files changed, 505 insertions(+), 401 deletions(-) create mode 100644 resources/exunit/1.1.0/team_city_ex_unit_formatter.ex create mode 100644 resources/exunit/1.4.0/team_city_ex_unit_formatter.ex rename resources/exunit/{team_city_ex_unit_formatter.ex => team_city_ex_unit_formatting.ex} (68%) diff --git a/resources/exunit/1.1.0/team_city_ex_unit_formatter.ex b/resources/exunit/1.1.0/team_city_ex_unit_formatter.ex new file mode 100644 index 000000000..09fc5e94f --- /dev/null +++ b/resources/exunit/1.1.0/team_city_ex_unit_formatter.ex @@ -0,0 +1,22 @@ +# Originally based on https://github.com/lixhq/teamcity-exunit-formatter, but it did not work for parallel tests: IDEA +# does not honor flowId, so needed to use the nodeId/parentNodeIde system +# +# nodeId and parentNodeId system is documented in +# https://intellij-support.jetbrains.com/hc/en-us/community/posts/115000389550/comments/115000330464 +defmodule TeamCityExUnitFormatter do + @moduledoc false + + use GenEvent + + # Functions + + ## GenEvent Callbacks + + def handle_event(event, state) do + updated_state = TeamCityExUnitFormatting.put_event(state, event) + + {:ok, updated_state} + end + + def init(opts), do: {:ok, TeamCityExUnitFormatting.new(opts)} +end diff --git a/resources/exunit/1.4.0/team_city_ex_unit_formatter.ex b/resources/exunit/1.4.0/team_city_ex_unit_formatter.ex new file mode 100644 index 000000000..f0520b175 --- /dev/null +++ b/resources/exunit/1.4.0/team_city_ex_unit_formatter.ex @@ -0,0 +1,17 @@ +defmodule TeamCityExUnitFormatter do + @moduledoc false + + use GenServer + + # Functions + + ## GenServer Callbacks + + def handle_cast(event, state) do + updated_state = TeamCityExUnitFormatting.put_event(state, event) + + {:noreply, updated_state} + end + + def init(opts), do: {:ok, TeamCityExUnitFormatting.new(opts)} +end diff --git a/resources/exunit/team_city_ex_unit_formatter.ex b/resources/exunit/team_city_ex_unit_formatting.ex similarity index 68% rename from resources/exunit/team_city_ex_unit_formatter.ex rename to resources/exunit/team_city_ex_unit_formatting.ex index 591340af4..ddad4d55f 100644 --- a/resources/exunit/team_city_ex_unit_formatter.ex +++ b/resources/exunit/team_city_ex_unit_formatting.ex @@ -3,41 +3,71 @@ # # nodeId and parentNodeId system is documented in # https://intellij-support.jetbrains.com/hc/en-us/community/posts/115000389550/comments/115000330464 -defmodule TeamCityExUnitFormatter do - @moduledoc false - - use GenEvent +defmodule TeamCityExUnitFormatting do + # Constants @root_parent_node_id 0 + # Struct + + defstruct failures_counter: 0, + invalids_counter: 0, + seed: nil, + skipped_counter: 0, + tests_counter: 0, + trace: false, + width: 80 + # Functions @doc false def formatter(_color, msg), do: msg - ## GenEvent Callbacks + def new(opts) do + { + :ok, + %__MODULE__{ + seed: opts[:seed], + trace: opts[:trace] + } + } + end - def handle_event({:case_finished, test_case = %ExUnit.TestCase{}}, config) do + def put_event(state, {:case_finished, test_case = %ExUnit.TestCase{}}) do put_formatted :test_suite_finished, attributes(test_case) - {:ok, config} + state end - def handle_event({:case_started, test_case = %ExUnit.TestCase{}}, config) do + def put_event(state, {:case_started, test_case = %ExUnit.TestCase{}}) do put_formatted :test_suite_started, attributes(test_case) - {:ok, config} + state end - def handle_event( - {:test_finished, test = %ExUnit.Test{state: {:failed, {_, reason, _} = failed},}, time: time}, - config + def put_event( + state = %__MODULE{ + failures_counter: failures_counter, + tests_counter: tests_counter, + width: width + }, + { + :test_finished, + test = %ExUnit.Test{ + state: failed = { + :failed, + {_, reason, _} + }, + time: time + } + } ) do + updated_failures_counter = failures_counter + 1 formatted = ExUnit.Formatter.format_test_failure( test, failed, - config.failures_counter + 1, - config.width, + updated_failures_counter, + width, &formatter/2 ) attributes = attributes(test) @@ -54,23 +84,27 @@ defmodule TeamCityExUnitFormatter do duration: div(time, 1000) ) - { - :ok, - %{ - config | - tests_counter: config.tests_counter + 1, - failures_counter: config.failures_counter + 1 - } + %{ + state | + tests_counter: tests_counter + 1, + failures_counter: updated_failures_counter } end - def handle_event({:test_finished, test = %ExUnit.Test{state: {:failed, failed}}, time: time}, config) - when is_list(failed) do + def put_event( + state = %__MODULE__{ + failures_counter: failures_counter, + width: width, + tests_counter: tests_counter + }, + {:test_finished, test = %ExUnit.Test{state: {:failed, failed}, time: time}} + ) when is_list(failed) do + updated_failures_counter = failures_counter + 1 formatted = ExUnit.Formatter.format_test_failure( test, failed, - config.failures_counter + 1, - config.width, + updated_failures_counter, + width, &formatter/2 ) message = Enum.map_join(failed, "", fn {_kind, reason, _stack} -> inspect(reason) end) @@ -88,33 +122,41 @@ defmodule TeamCityExUnitFormatter do duration: div(time, 1000) ) - { - :ok, - %{ - config | - tests_counter: config.tests_counter + 1, - failures_counter: config.failures_counter + 1 - } + %{ + state | + tests_counter: tests_counter + 1, + failures_counter: updated_failures_counter } end - def handle_event({:test_finished, test = %ExUnit.Test{state: {:skip, _}}}, config) do + def put_event( + state = %__MODULE__{ + tests_counter: tests_counter, + skipped_counter: skipped_counter + }, + {:test_finished, test = %ExUnit.Test{state: {:skip, _}}} + ) do attributes = attributes(test) put_formatted :test_ignored, attributes put_formatted :test_finished, attributes - { - :ok, - %{ - config | - tests_counter: config.tests_counter + 1, - skipped_counter: config.skipped_counter + 1 - } + %{ + state | + tests_counter: tests_counter + 1, + skipped_counter: skipped_counter + 1 } end - def handle_event({:test_finished, test = %ExUnit.Test{time: time}}, config) do + def put_event( + state, + { + :test_finished, + test = %ExUnit.Test{ + time: time + } + } + ) do put_formatted :test_finished, test |> attributes() @@ -122,10 +164,10 @@ defmodule TeamCityExUnitFormatter do duration: div(time, 1000) ) - {:ok, config} + state end - def handle_event({:test_started, test = %ExUnit.Test{tags: tags}}, config) do + def put_event(state, {:test_started, test = %ExUnit.Test{tags: tags}}) do put_formatted :test_started, test |> attributes() @@ -133,27 +175,10 @@ defmodule TeamCityExUnitFormatter do locationHint: "file://#{tags[:file]}:#{tags[:line]}" ) - {:ok, config} - end - - def handle_event(_, config) do - {:ok, config} + state end - def init(opts) do - { - :ok, - %{ - failures_counter: 0, - invalids_counter: 0, - seed: opts[:seed], - skipped_counter: 0, - tests_counter: 0, - trace: opts[:trace], - width: 80 - } - } - end + def put_event(_, state), do: state ## Private Functions diff --git a/src/org/elixir_lang/debugger/xdebug/ElixirXDebugProcess.java b/src/org/elixir_lang/debugger/xdebug/ElixirXDebugProcess.java index 45d7d3cb0..46153e6ba 100644 --- a/src/org/elixir_lang/debugger/xdebug/ElixirXDebugProcess.java +++ b/src/org/elixir_lang/debugger/xdebug/ElixirXDebugProcess.java @@ -20,7 +20,9 @@ import com.ericsson.otp.erlang.OtpErlangPid; import com.intellij.execution.ExecutionException; +import com.intellij.execution.RunnerAndConfigurationSettings; import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.configurations.RunConfiguration; import com.intellij.execution.process.OSProcessHandler; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.ExecutionEnvironment; @@ -342,7 +344,14 @@ private OSProcessHandler runDebugTarget() throws ExecutionException { LOG.debug("Preparing to run debug target."); ArrayList elixirParams = new ArrayList<>(); - elixirParams.addAll(myRunningState.setupElixirParams()); + RunnerAndConfigurationSettings runnerAndConfigurationSettings = myExecutionEnvironment.getRunnerAndConfigurationSettings(); + RunConfiguration runConfiguration = null; + + if (runnerAndConfigurationSettings != null) { + runConfiguration = runnerAndConfigurationSettings.getConfiguration(); + } + + elixirParams.addAll(myRunningState.setupElixirParams(runConfiguration)); elixirParams.addAll(setUpElixirDebuggerCodePath()); List mixParams = new ArrayList<>(); diff --git a/src/org/elixir_lang/exunit/ElixirModules.java b/src/org/elixir_lang/exunit/ElixirModules.java index 7f90cdc49..6cba5c76f 100644 --- a/src/org/elixir_lang/exunit/ElixirModules.java +++ b/src/org/elixir_lang/exunit/ElixirModules.java @@ -1,43 +1,73 @@ package org.elixir_lang.exunit; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.ResourceUtil; import com.intellij.util.io.URLUtil; +import org.elixir_lang.sdk.ElixirSdkRelease; +import org.elixir_lang.sdk.ElixirSdkType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.*; import java.net.URL; public class ElixirModules { - private static final String FORMATTER_FILE_NAME = "team_city_ex_unit_formatter.ex"; - private static final String MIX_TASK_FILE_NAME = "test_with_formatter.ex"; + private static final String FORMATTER_FILE_NAME = "team_city_ex_unit_formatter.ex"; + private static final String FORMATTING_FILE_NAME = "team_city_ex_unit_formatting.ex"; + private static final String MIX_TASK_FILE_NAME = "test_with_formatter.ex"; - private ElixirModules() { - } + private ElixirModules() { + } + + private static File putFile(@NotNull String relativePath, @NotNull File directory) throws IOException { + URL moduleUrl = ResourceUtil.getResource(ElixirModules.class, "/exunit", relativePath); + + if (moduleUrl == null) { + throw new IOException("Failed to locate Elixir module " + relativePath); + } + + try (BufferedInputStream inputStream = new BufferedInputStream(URLUtil.openStream(moduleUrl))) { + File file = new File(directory, relativePath); + file.getParentFile().mkdirs(); - private static File putFile(@NotNull String fileName, @NotNull File directory) throws IOException { - URL moduleUrl = ResourceUtil.getResource(ElixirModules.class, "/exunit", fileName); - if (moduleUrl == null) { - throw new IOException("Failed to locate Elixir module " + fileName); + try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file))) { + FileUtil.copy(inputStream, outputStream); + return file; + } + } } - BufferedInputStream inputStream = new BufferedInputStream(URLUtil.openStream(moduleUrl)); - File file = new File(directory, fileName); - BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file)); - try { - FileUtil.copy(inputStream, outputStream); - return file; - } finally { - inputStream.close(); - outputStream.close(); + public static File putFormatterTo(@NotNull File directory, @Nullable Project project) throws IOException { + Sdk sdk = null; + + if (project != null) { + sdk = ProjectRootManager.getInstance(project).getProjectSdk(); + } + + return putFormatterTo(directory, sdk); } - } - public static File putFormatterTo(@NotNull File directory) throws IOException { - return putFile(FORMATTER_FILE_NAME, directory); - } + private static File putFormatterTo(@NotNull File directory, @Nullable Sdk sdk) throws IOException { + String versionDirectory = "1.4.0"; + + ElixirSdkRelease release = ElixirSdkType.getRelease(sdk); - public static File putMixTaskTo(@NotNull File directory) throws IOException { - return putFile(MIX_TASK_FILE_NAME, directory); - } + if (release != null && release.compareTo(ElixirSdkRelease.V_1_4) < 0) { + versionDirectory = "1.1.0"; + } + + String source = versionDirectory + "/" + FORMATTER_FILE_NAME; + return putFile(source, directory); + } + + public static File putFormattingTo(@NotNull File directory) throws IOException { + return putFile(FORMATTING_FILE_NAME, directory); + } + + public static File putMixTaskTo(@NotNull File directory) throws IOException { + return putFile(MIX_TASK_FILE_NAME, directory); + } } diff --git a/src/org/elixir_lang/mix/runner/MixRunningState.java b/src/org/elixir_lang/mix/runner/MixRunningState.java index d8ff6e2d1..123e60c2b 100644 --- a/src/org/elixir_lang/mix/runner/MixRunningState.java +++ b/src/org/elixir_lang/mix/runner/MixRunningState.java @@ -5,6 +5,7 @@ import com.intellij.execution.Executor; import com.intellij.execution.configurations.CommandLineState; import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.configurations.RunConfiguration; import com.intellij.execution.filters.TextConsoleBuilder; import com.intellij.execution.filters.TextConsoleBuilderFactory; import com.intellij.execution.filters.TextConsoleBuilderImpl; @@ -14,6 +15,7 @@ import com.intellij.execution.ui.ConsoleView; import org.elixir_lang.console.ElixirConsoleUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -51,7 +53,7 @@ public ConsoleView getConsole() { @Override protected ProcessHandler startProcess() throws ExecutionException { GeneralCommandLine commandLine = MixRunningStateUtil.commandLine( - myConfiguration, setupElixirParams(), myConfiguration.getMixArgs() + myConfiguration, setupElixirParams(myConfiguration), myConfiguration.getMixArgs() ); return runMix(myConfiguration.getProject(), commandLine); } @@ -63,7 +65,7 @@ public ConsoleView createConsoleView(Executor executor) { } @NotNull - public List setupElixirParams() throws ExecutionException { + public List setupElixirParams(@Nullable RunConfiguration runConfiguration) throws ExecutionException { return new ArrayList<>(); } } diff --git a/src/org/elixir_lang/mix/runner/exunit/MixExUnitRunningState.java b/src/org/elixir_lang/mix/runner/exunit/MixExUnitRunningState.java index 7fcedff52..1cabec5f0 100644 --- a/src/org/elixir_lang/mix/runner/exunit/MixExUnitRunningState.java +++ b/src/org/elixir_lang/mix/runner/exunit/MixExUnitRunningState.java @@ -4,6 +4,7 @@ import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionResult; import com.intellij.execution.Executor; +import com.intellij.execution.configurations.RunConfiguration; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.runners.ProgramRunner; @@ -11,6 +12,7 @@ import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil; import com.intellij.execution.ui.ConsoleView; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; import org.elixir_lang.console.ElixirConsoleUtil; import org.elixir_lang.exunit.ElixirModules; @@ -18,6 +20,7 @@ import org.elixir_lang.mix.runner.MixTestConsoleProperties; import org.elixir_lang.mix.settings.MixSettings; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; @@ -151,13 +154,21 @@ private static List elixirParams(@NotNull Collection elixirModuleF } @NotNull - public List setupElixirParams() throws ExecutionException { + public List setupElixirParams(@Nullable RunConfiguration runConfiguration) throws ExecutionException { List elixirModuleFileList = new ArrayList<>(); try { File elixirModulesDir = createElixirModulesDirectory(); - elixirModuleFileList.add(ElixirModules.putFormatterTo(elixirModulesDir)); + elixirModuleFileList.add(ElixirModules.putFormattingTo(elixirModulesDir)); + + Project project = null; + + if (runConfiguration != null) { + project = runConfiguration.getProject(); + } + + elixirModuleFileList.add(ElixirModules.putFormatterTo(elixirModulesDir, project)); // Support for the --formatter option was recently added to Mix. Older versions of Elixir will need to use the // custom task we've included in order to support this option diff --git a/src/org/elixir_lang/sdk/ElixirSdkType.java b/src/org/elixir_lang/sdk/ElixirSdkType.java index d4b1318d1..0d5ad990a 100644 --- a/src/org/elixir_lang/sdk/ElixirSdkType.java +++ b/src/org/elixir_lang/sdk/ElixirSdkType.java @@ -15,7 +15,6 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiElement; -import com.intellij.util.Function; import org.elixir_lang.icons.ElixirIcons; import org.elixir_lang.jps.model.JpsElixirModelSerializerExtension; import org.elixir_lang.jps.model.JpsElixirSdkType; @@ -31,367 +30,356 @@ import static org.elixir_lang.sdk.ElixirSystemUtil.STANDARD_TIMEOUT; import static org.elixir_lang.sdk.ElixirSystemUtil.transformStdoutLine; -/** - * Created by zyuyou on 2015/5/27. - * - */ public class ElixirSdkType extends SdkType { - private final Map mySdkHomeToReleaseCache = ApplicationManager.getApplication().isUnitTestMode() ? - new HashMap() : new WeakHashMap(); - - private static final Logger LOG = Logger.getInstance(ElixirSdkType.class); - - public ElixirSdkType() { - super(JpsElixirModelSerializerExtension.ELIXIR_SDK_TYPE_ID); - } - - @NotNull - public static ElixirSdkType getInstance(){ - ElixirSdkType instance = SdkType.findInstance(ElixirSdkType.class); - assert instance != null : "Make sure ElixirSdkType is registered in plugin.xml"; - return instance; - } - - @Override - public Icon getIcon() { - return ElixirIcons.FILE; - } - - @Override - public Icon getIconForAddAction() { - return getIcon(); - } - - @Nullable - @Override - public String suggestHomePath() { - Iterator iterator = suggestHomePaths().iterator(); - String suggestedHomePath = null; - - if (iterator.hasNext()) { - suggestedHomePath = iterator.next(); + private static final Logger LOG = Logger.getInstance(ElixirSdkType.class); + private final Map mySdkHomeToReleaseCache = + ApplicationManager.getApplication().isUnitTestMode() ? new HashMap<>() : new WeakHashMap<>(); + + public ElixirSdkType() { + super(JpsElixirModelSerializerExtension.ELIXIR_SDK_TYPE_ID); } - return suggestedHomePath; - } - - @Override - public Collection suggestHomePaths() { - return homePathByVersion().values(); - } - - @Override - public boolean isValidSdkHome(@NotNull String path) { - File elixir = JpsElixirSdkType.getScriptInterpreterExecutable(path); - File elixirc = JpsElixirSdkType.getByteCodeCompilerExecutable(path); - File iex = JpsElixirSdkType.getIExExecutable(path); - File mix = JpsElixirSdkType.getMixExecutable(path); - - // Determine whether everything is executable - return elixir.canExecute() && elixirc.canExecute() && iex.canExecute() && mix.canExecute(); - } - - @Override - public String suggestSdkName(@Nullable String currentSdkName, @NotNull String sdkHome) { - return getDefaultSdkName(sdkHome, detectSdkVersion(sdkHome)); - } - - @Nullable - @Override - public String getVersionString(@NotNull String sdkHome) { - return getVersionString(detectSdkVersion(sdkHome)); - } - - @Nullable - @Override - public String getDefaultDocumentationUrl(@NotNull Sdk sdk) { - return getDefaultDocumentationUrl(getRelease(sdk)); - } - - @Nullable - @Override - public AdditionalDataConfigurable createAdditionalDataConfigurable(@NotNull SdkModel sdkModel, @NotNull SdkModificator sdkModificator) { - return null; - } - - @Override - public String getPresentableName() { - return "Elixir SDK"; - } - - @Override - public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) { - } - - @Override - public void setupSdkPaths(@NotNull Sdk sdk) { - configureSdkPaths(sdk); - } - - @Nullable - public static String getSdkPath(@NotNull final Project project){ - // todo small ide - if(ElixirSystemUtil.isSmallIde()){ - return ElixirSdkForSmallIdes.getSdkHome(project); + private static void configureSdkPaths(@NotNull Sdk sdk) { + SdkModificator sdkModificator = sdk.getSdkModificator(); + setupLocalSdkPaths(sdkModificator); + String externalDocUrl = getDefaultDocumentationUrl(getRelease(sdk)); + if (externalDocUrl != null) { + VirtualFile fileByUrl = VirtualFileManager.getInstance().findFileByUrl(externalDocUrl); + + if (fileByUrl != null) { + sdkModificator.addRoot(fileByUrl, JavadocOrderRootType.getInstance()); + } + } + + sdkModificator.commitChanges(); } - Sdk sdk = ProjectRootManager.getInstance(project).getProjectSdk(); - return sdk != null && sdk.getSdkType() == getInstance() ? sdk.getHomePath() : null; - } + @TestOnly + @NotNull + public static Sdk createMockSdk(@NotNull String sdkHome, @NotNull ElixirSdkRelease version) { + getInstance().mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), version); // we'll not try to detect sdk version in tests environment + Sdk sdk = new ProjectJdkImpl(getDefaultSdkName(sdkHome, version), getInstance()); + SdkModificator sdkModificator = sdk.getSdkModificator(); + sdkModificator.setHomePath(sdkHome); + sdkModificator.setVersionString(getVersionString(version));// must be set after home path, otherwise setting home path clears the version string + sdkModificator.commitChanges(); + configureSdkPaths(sdk); + return sdk; + } - @NotNull - public static ElixirSdkRelease getNonNullRelease(@NotNull PsiElement element) { - ElixirSdkRelease nonNullRelease = getRelease(element); + @Nullable + private static String getDefaultDocumentationUrl(@Nullable ElixirSdkRelease version) { + return version == null ? null : "http://elixir-lang.org/docs/stable/elixir/"; + } - if (nonNullRelease == null) { - nonNullRelease = ElixirSdkRelease.LATEST; + @NotNull + private static String getDefaultSdkName(@NotNull String sdkHome, @Nullable ElixirSdkRelease release) { + return release != null ? release.toString() : "Unknown Elixir version at " + sdkHome; } - return nonNullRelease; - } + @NotNull + public static ElixirSdkType getInstance() { + return SdkType.findInstance(ElixirSdkType.class); + } - @Nullable - public static ElixirSdkRelease getRelease(@NotNull PsiElement element) { - ElixirSdkRelease release = null; - Project project = element.getProject(); + @NotNull + public static ElixirSdkRelease getNonNullRelease(@NotNull PsiElement element) { + ElixirSdkRelease nonNullRelease = getRelease(element); - if (ElixirSystemUtil.isSmallIde()) { - release = getReleaseForSmallIde(project); - } else { + if (nonNullRelease == null) { + nonNullRelease = ElixirSdkRelease.LATEST; + } + + return nonNullRelease; + } + + @Nullable + public static ElixirSdkRelease getRelease(@NotNull PsiElement element) { + ElixirSdkRelease release = null; + Project project = element.getProject(); + + if (ElixirSystemUtil.isSmallIde()) { + release = getReleaseForSmallIde(project); + } else { /* 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) { - Module module = ModuleUtilCore.findModuleForPsiElement(element); + if (ProjectFileIndex.SERVICE.getInstance(project) != null) { + Module module = ModuleUtilCore.findModuleForPsiElement(element); + + if (module != null) { + @Nullable ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); - if (module != null) { - @Nullable ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); + if (moduleRootManager != null) { + Sdk sdk = moduleRootManager.getSdk(); - if (moduleRootManager != null) { - Sdk sdk = moduleRootManager.getSdk(); + if (sdk != null) { + release = getRelease(sdk); + } + } + } + } - if (sdk != null) { - release = getRelease(sdk); + if (release == null) { + release = getRelease(project); } - } } - } - if (release == null) { - release = getRelease(project); - } + return release; } - return release; - } + @Nullable + private static ElixirSdkRelease getRelease(@NotNull Project project) { + ElixirSdkRelease release; - @Nullable - public static ElixirSdkRelease getRelease(@NotNull Project project){ - ElixirSdkRelease release; + if (ElixirSystemUtil.isSmallIde()) { + release = getReleaseForSmallIde(project); + } else { + ProjectRootManager projectRootManager = ProjectRootManager.getInstance(project); - if (ElixirSystemUtil.isSmallIde()) { - release = getReleaseForSmallIde(project); - } else { - ProjectRootManager projectRootManager = ProjectRootManager.getInstance(project); + if (projectRootManager != null) { + release = getRelease(projectRootManager.getProjectSdk()); + } else { + release = null; + } + } - if (projectRootManager != null) { - release = getRelease(projectRootManager.getProjectSdk()); - } else { - release = null; - } + return release; } - return release; - } + @Nullable + public static ElixirSdkRelease getRelease(@Nullable Sdk sdk) { + if (sdk != null && sdk.getSdkType() == getInstance()) { + ElixirSdkRelease fromVersionString = ElixirSdkRelease.fromString(sdk.getVersionString()); + return fromVersionString != null ? fromVersionString : getInstance().detectSdkVersion(StringUtil.notNullize(sdk.getHomePath())); + } + return null; + } + @Nullable + private static ElixirSdkRelease getReleaseForSmallIde(@NotNull Project project) { + String sdkPath = getSdkPath(project); + return StringUtil.isEmpty(sdkPath) ? null : getInstance().detectSdkVersion(sdkPath); + } - @NotNull - private static String getDefaultSdkName(@NotNull String sdkHome, @Nullable ElixirSdkRelease release){ - return release != null ? release.toString() : "Unknown Elixir version at " + sdkHome ; - } + @Nullable + public static String getSdkPath(@NotNull final Project project) { + // todo small ide + if (ElixirSystemUtil.isSmallIde()) { + return ElixirSdkForSmallIdes.getSdkHome(project); + } - @Nullable - public ElixirSdkRelease detectSdkVersion(@NotNull String sdkHome){ - ElixirSdkRelease cachedRelease = mySdkHomeToReleaseCache.get(getVersionCacheKey(sdkHome)); - if(cachedRelease != null){ - return cachedRelease; + Sdk sdk = ProjectRootManager.getInstance(project).getProjectSdk(); + return sdk != null && sdk.getSdkType() == getInstance() ? sdk.getHomePath() : null; } - File elixir = JpsElixirSdkType.getScriptInterpreterExecutable(sdkHome); - if(!elixir.canExecute()){ - String reason = elixir.getPath() + (elixir.exists() ? " is not executable." : " is missing."); - LOG.warn("Can't detect Elixir version: " + reason); - return null; + @Nullable + private static String getVersionCacheKey(@Nullable String sdkHome) { + return sdkHome != null ? new File(sdkHome).getAbsolutePath() : null; } - ElixirSdkRelease release = transformStdoutLine( - new Function() { - @Override - public ElixirSdkRelease fun(String line) { - return ElixirSdkRelease.fromString(line); - } - }, - STANDARD_TIMEOUT, - sdkHome, - elixir.getAbsolutePath(), - "-e", - "IO.puts System.build_info[:version]" - ); - - if (release != null) { - mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), release); + @Nullable + private static String getVersionString(@Nullable ElixirSdkRelease version) { + return version != null ? version.toString() : null; } - return release; - } - - @Nullable - private static String getVersionCacheKey(@Nullable String sdkHome){ - return sdkHome != null ? new File(sdkHome).getAbsolutePath() : null; - } - - @Nullable - private static String getVersionString(@Nullable ElixirSdkRelease version) { - return version != null ? version.toString() : null; - } - - @Nullable - private static String getDefaultDocumentationUrl(@Nullable ElixirSdkRelease version) { - return version == null ? null : "http://elixir-lang.org/docs/stable/elixir/"; - } - - @Nullable - private static ElixirSdkRelease getRelease(@Nullable Sdk sdk) { - if (sdk != null && sdk.getSdkType() == getInstance()) { - ElixirSdkRelease fromVersionString = ElixirSdkRelease.fromString(sdk.getVersionString()); - return fromVersionString != null ? fromVersionString : getInstance().detectSdkVersion(StringUtil.notNullize(sdk.getHomePath())); + private static boolean isStandardLibraryDir(@NotNull File dir) { + return dir.isDirectory(); } - return null; - } - - private static void configureSdkPaths(@NotNull Sdk sdk) { - SdkModificator sdkModificator = sdk.getSdkModificator(); - setupLocalSdkPaths(sdkModificator); - String externalDocUrl = getDefaultDocumentationUrl(getRelease(sdk)); - if (externalDocUrl != null) { - VirtualFile fileByUrl = VirtualFileManager.getInstance().findFileByUrl(externalDocUrl); - - if (fileByUrl != null) { - sdkModificator.addRoot(fileByUrl, JavadocOrderRootType.getInstance()); - } + + private static void setupLocalSdkPaths(@NotNull SdkModificator sdkModificator) { + String sdkHome = sdkModificator.getHomePath(); + + { + File stdLibDir = new File(new File(sdkHome), "lib"); + if (tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir)) { + return; + } + } + + assert !ApplicationManager.getApplication().isUnitTestMode() : "Failed to setup a mock SDK"; + + File stdLibDir = new File("/usr/lib/erlang"); + tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir); } - sdkModificator.commitChanges(); - } + /** + * set the sdk libs + * todo: differentiating `Elixir.*.beam` files and `*.ex` files. + */ + private static boolean tryToProcessAsStandardLibraryDir(@NotNull SdkModificator sdkModificator, @NotNull File stdLibDir) { + if (!isStandardLibraryDir(stdLibDir)) { + return false; + } + VirtualFile dir = LocalFileSystem.getInstance().findFileByIoFile(stdLibDir); + if (dir != null) { + sdkModificator.addRoot(dir, OrderRootType.SOURCES); + sdkModificator.addRoot(dir, OrderRootType.CLASSES); + } + return true; + } - private static void setupLocalSdkPaths(@NotNull SdkModificator sdkModificator){ - String sdkHome = sdkModificator.getHomePath(); + @Nullable + @Override + public AdditionalDataConfigurable createAdditionalDataConfigurable(@NotNull SdkModel sdkModel, @NotNull SdkModificator sdkModificator) { + return null; + } - { - File stdLibDir = new File(new File(sdkHome), "lib"); - if(tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir)) return; + @Nullable + private ElixirSdkRelease detectSdkVersion(@NotNull String sdkHome) { + ElixirSdkRelease cachedRelease = mySdkHomeToReleaseCache.get(getVersionCacheKey(sdkHome)); + if (cachedRelease != null) { + return cachedRelease; + } + + File elixir = JpsElixirSdkType.getScriptInterpreterExecutable(sdkHome); + if (!elixir.canExecute()) { + String reason = elixir.getPath() + (elixir.exists() ? " is not executable." : " is missing."); + LOG.warn("Can't detect Elixir version: " + reason); + return null; + } + + ElixirSdkRelease release = transformStdoutLine( + ElixirSdkRelease::fromString, + STANDARD_TIMEOUT, + sdkHome, + elixir.getAbsolutePath(), + "-e", + "IO.puts System.build_info[:version]" + ); + + if (release != null) { + mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), release); + } + + return release; } - assert !ApplicationManager.getApplication().isUnitTestMode() : "Failed to setup a mock SDK"; - - File stdLibDir = new File("/usr/lib/erlang"); - tryToProcessAsStandardLibraryDir(sdkModificator, stdLibDir); - } - - /** - * set the sdk libs - * todo: differentiating `Elixir.*.beam` files and `*.ex` files. - * */ - private static boolean tryToProcessAsStandardLibraryDir(@NotNull SdkModificator sdkModificator, @NotNull File stdLibDir) { - if (!isStandardLibraryDir(stdLibDir)) return false; - VirtualFile dir = LocalFileSystem.getInstance().findFileByIoFile(stdLibDir); - if (dir != null) { - sdkModificator.addRoot(dir, OrderRootType.SOURCES); - sdkModificator.addRoot(dir, OrderRootType.CLASSES); + @Nullable + @Override + public String getDefaultDocumentationUrl(@NotNull Sdk sdk) { + return getDefaultDocumentationUrl(getRelease(sdk)); } - return true; - } - - private static boolean isStandardLibraryDir(@NotNull File dir) { - return dir.isDirectory(); - } - - /** - * Map of home paths to versions in descending version order so that newer versions are favored. - * - * @return Map - */ - private Map homePathByVersion() { - Map homePathByVersion = new TreeMap( - new Comparator() { - @Override - public int compare(Version version1, Version version2) { - // compare version2 to version1 to produce descending instead of ascending order. - return version2.compareTo(version1); - } - } - ); - - if (SystemInfo.isMac) { - File homebrewRoot = new File("/usr/local/Cellar/elixir"); - - if (homebrewRoot.isDirectory()) { - File[] files = homebrewRoot.listFiles(); - if(files != null){ - for (File child : files) { - if (child.isDirectory()) { - String versionString = child.getName(); - String[] versionParts = versionString.split("\\.", 3); - int major = Integer.parseInt(versionParts[0]); - int minor = Integer.parseInt(versionParts[1]); - int bugfix = Integer.parseInt(versionParts[2]); - Version version = new Version(major, minor, bugfix); - - homePathByVersion.put(version, child.getAbsolutePath()); + + @Override + public Icon getIcon() { + return ElixirIcons.FILE; + } + + @NotNull + @Override + public Icon getIconForAddAction() { + return getIcon(); + } + + @NotNull + @Override + public String getPresentableName() { + return "Elixir SDK"; + } + + @Nullable + @Override + public String getVersionString(@NotNull String sdkHome) { + return getVersionString(detectSdkVersion(sdkHome)); + } + + /** + * Map of home paths to versions in descending version order so that newer versions are favored. + * + * @return Map + */ + private Map homePathByVersion() { + Map homePathByVersion = new TreeMap<>( + (version1, version2) -> { + // compare version2 to version1 to produce descending instead of ascending order. + return version2.compareTo(version1); + } + ); + + if (SystemInfo.isMac) { + File homebrewRoot = new File("/usr/local/Cellar/elixir"); + + if (homebrewRoot.isDirectory()) { + File[] files = homebrewRoot.listFiles(); + if (files != null) { + for (File child : files) { + if (child.isDirectory()) { + String versionString = child.getName(); + String[] versionParts = versionString.split("\\.", 3); + int major = Integer.parseInt(versionParts[0]); + int minor = Integer.parseInt(versionParts[1]); + int bugfix = Integer.parseInt(versionParts[2]); + Version version = new Version(major, minor, bugfix); + + homePathByVersion.put(version, child.getAbsolutePath()); + } + } + } } - } - } - } - } else { - Version version = new Version(0,0,0); - String sdkPath = ""; - - if (SystemInfo.isWindows){ - if (SystemInfo.is32Bit){ - sdkPath = "C:\\Program Files\\Elixir"; } else { - sdkPath = "C:\\Program Files (x86)\\Elixir"; + Version version = new Version(0, 0, 0); + String sdkPath = ""; + + if (SystemInfo.isWindows) { + if (SystemInfo.is32Bit) { + sdkPath = "C:\\Program Files\\Elixir"; + } else { + sdkPath = "C:\\Program Files (x86)\\Elixir"; + } + } else if (SystemInfo.isLinux) { + sdkPath = "/usr/local/lib/elixir"; + } + + homePathByVersion.put(version, sdkPath); } - } else if (SystemInfo.isLinux){ - sdkPath = "/usr/local/lib/elixir"; - } - homePathByVersion.put(version, sdkPath); + return homePathByVersion; + } + + @Override + public boolean isValidSdkHome(@NotNull String path) { + File elixir = JpsElixirSdkType.getScriptInterpreterExecutable(path); + File elixirc = JpsElixirSdkType.getByteCodeCompilerExecutable(path); + File iex = JpsElixirSdkType.getIExExecutable(path); + File mix = JpsElixirSdkType.getMixExecutable(path); + + // Determine whether everything is executable + return elixir.canExecute() && elixirc.canExecute() && iex.canExecute() && mix.canExecute(); + } + + @Override + public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) { } - return homePathByVersion; - } - - @Nullable - private static ElixirSdkRelease getReleaseForSmallIde(@NotNull Project project){ - String sdkPath = getSdkPath(project); - return StringUtil.isEmpty(sdkPath) ? null : getInstance().detectSdkVersion(sdkPath); - } - - @TestOnly - @NotNull - public static Sdk createMockSdk(@NotNull String sdkHome, @NotNull ElixirSdkRelease version){ - getInstance().mySdkHomeToReleaseCache.put(getVersionCacheKey(sdkHome), version); // we'll not try to detect sdk version in tests environment - Sdk sdk = new ProjectJdkImpl(getDefaultSdkName(sdkHome, version), getInstance()); - SdkModificator sdkModificator = sdk.getSdkModificator(); - sdkModificator.setHomePath(sdkHome); - sdkModificator.setVersionString(getVersionString(version));// must be set after home path, otherwise setting home path clears the version string - sdkModificator.commitChanges(); - configureSdkPaths(sdk); - return sdk; - } + @Override + public void setupSdkPaths(@NotNull Sdk sdk) { + configureSdkPaths(sdk); + } + + @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 String suggestSdkName(@Nullable String currentSdkName, @NotNull String sdkHome) { + return getDefaultSdkName(sdkHome, detectSdkVersion(sdkHome)); + } }